From cc38220128c6ef93d774c2e6300616cd9a92e96e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 02:26:52 +0000 Subject: [PATCH 1/3] Initial plan From 7d8dd013d6b22f28541f83f6d4114c0a93cec4da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 02:38:55 +0000 Subject: [PATCH 2/3] Add Resolver Agent - 6th agent for conflict resolution and system resilience Co-authored-by: Stacey77 <54900383+Stacey77@users.noreply.github.com> --- agents/__init__.py | 16 + agents/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 528 bytes agents/__pycache__/base_agent.cpython-312.pyc | Bin 0 -> 8514 bytes .../resolver_agent.cpython-312.pyc | Bin 0 -> 14109 bytes agents/base_agent.py | 155 +++++++ agents/resolver_agent.py | 257 ++++++++++++ config/priorities.yaml | 28 ++ config/recovery_strategies.yaml | 87 ++++ config/resolver_config.yaml | 81 ++++ dashboard/__init__.py | 7 + dashboard/health_dashboard.py | 130 ++++++ dashboard/static/css/dashboard.css | 84 ++++ dashboard/static/js/dashboard.js | 87 ++++ dashboard/templates/dashboard.html | 37 ++ docs/conflict_resolution_guide.md | 97 +++++ docs/error_recovery_guide.md | 80 ++++ docs/resolver_agent.md | 114 ++++++ examples/conflict_resolution_demo.py | 120 ++++++ examples/error_recovery_demo.py | 87 ++++ examples/health_monitoring_demo.py | 108 +++++ resolver/__init__.py | 28 ++ resolver/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1006 bytes .../__pycache__/arbitrator.cpython-312.pyc | Bin 0 -> 9829 bytes .../conflict_resolution.cpython-312.pyc | Bin 0 -> 19103 bytes .../deadlock_detector.cpython-312.pyc | Bin 0 -> 8624 bytes .../error_recovery.cpython-312.pyc | Bin 0 -> 11129 bytes .../fallback_planner.cpython-312.pyc | Bin 0 -> 7650 bytes .../health_monitor.cpython-312.pyc | Bin 0 -> 12487 bytes .../__pycache__/predictor.cpython-312.pyc | Bin 0 -> 10653 bytes resolver/arbitrator.py | 213 ++++++++++ resolver/conflict_resolution.py | 379 ++++++++++++++++++ resolver/deadlock_detector.py | 180 +++++++++ resolver/error_recovery.py | 208 ++++++++++ resolver/fallback_planner.py | 204 ++++++++++ resolver/health_monitor.py | 224 +++++++++++ resolver/predictor.py | 211 ++++++++++ ros2_interface/__init__.py | 3 + ros2_interface/launch/resolver.launch.py | 28 ++ ros2_interface/resolver_node.py | 268 +++++++++++++ tests/__init__.py | 3 + tests/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 180 bytes ...st_arbitrator.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 6995 bytes ...ct_resolution.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 12545 bytes ...rror_recovery.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 9168 bytes ...ealth_monitor.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 10181 bytes ...t_integration.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 11188 bytes ...esolver_agent.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 15153 bytes tests/test_arbitrator.py | 129 ++++++ tests/test_conflict_resolution.py | 190 +++++++++ tests/test_error_recovery.py | 109 +++++ tests/test_health_monitor.py | 132 ++++++ tests/test_integration.py | 183 +++++++++ tests/test_resolver_agent.py | 213 ++++++++++ 53 files changed, 4480 insertions(+) create mode 100644 agents/__init__.py create mode 100644 agents/__pycache__/__init__.cpython-312.pyc create mode 100644 agents/__pycache__/base_agent.cpython-312.pyc create mode 100644 agents/__pycache__/resolver_agent.cpython-312.pyc create mode 100644 agents/base_agent.py create mode 100644 agents/resolver_agent.py create mode 100644 config/priorities.yaml create mode 100644 config/recovery_strategies.yaml create mode 100644 config/resolver_config.yaml create mode 100644 dashboard/__init__.py create mode 100644 dashboard/health_dashboard.py create mode 100644 dashboard/static/css/dashboard.css create mode 100644 dashboard/static/js/dashboard.js create mode 100644 dashboard/templates/dashboard.html create mode 100644 docs/conflict_resolution_guide.md create mode 100644 docs/error_recovery_guide.md create mode 100644 docs/resolver_agent.md create mode 100644 examples/conflict_resolution_demo.py create mode 100644 examples/error_recovery_demo.py create mode 100644 examples/health_monitoring_demo.py create mode 100644 resolver/__init__.py create mode 100644 resolver/__pycache__/__init__.cpython-312.pyc create mode 100644 resolver/__pycache__/arbitrator.cpython-312.pyc create mode 100644 resolver/__pycache__/conflict_resolution.cpython-312.pyc create mode 100644 resolver/__pycache__/deadlock_detector.cpython-312.pyc create mode 100644 resolver/__pycache__/error_recovery.cpython-312.pyc create mode 100644 resolver/__pycache__/fallback_planner.cpython-312.pyc create mode 100644 resolver/__pycache__/health_monitor.cpython-312.pyc create mode 100644 resolver/__pycache__/predictor.cpython-312.pyc create mode 100644 resolver/arbitrator.py create mode 100644 resolver/conflict_resolution.py create mode 100644 resolver/deadlock_detector.py create mode 100644 resolver/error_recovery.py create mode 100644 resolver/fallback_planner.py create mode 100644 resolver/health_monitor.py create mode 100644 resolver/predictor.py create mode 100644 ros2_interface/__init__.py create mode 100644 ros2_interface/launch/resolver.launch.py create mode 100644 ros2_interface/resolver_node.py create mode 100644 tests/__init__.py create mode 100644 tests/__pycache__/__init__.cpython-312.pyc create mode 100644 tests/__pycache__/test_arbitrator.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_conflict_resolution.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_error_recovery.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_health_monitor.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_integration.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_resolver_agent.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/test_arbitrator.py create mode 100644 tests/test_conflict_resolution.py create mode 100644 tests/test_error_recovery.py create mode 100644 tests/test_health_monitor.py create mode 100644 tests/test_integration.py create mode 100644 tests/test_resolver_agent.py diff --git a/agents/__init__.py b/agents/__init__.py new file mode 100644 index 0000000..36401d3 --- /dev/null +++ b/agents/__init__.py @@ -0,0 +1,16 @@ +""" +Agents package for the Agentic AGI robotics system. + +Contains all agent implementations including the Resolver Agent. +""" + +from agents.base_agent import AgentMessage, AgentState, AgentStatus, BaseAgent +from agents.resolver_agent import ResolverAgent + +__all__ = [ + "BaseAgent", + "AgentStatus", + "AgentState", + "AgentMessage", + "ResolverAgent", +] diff --git a/agents/__pycache__/__init__.cpython-312.pyc b/agents/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..17a7cc68048e8cb125de42a875e69c78005713de GIT binary patch literal 528 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 literal 0 HcmV?d00001 diff --git a/agents/base_agent.py b/agents/base_agent.py new file mode 100644 index 0000000..518e148 --- /dev/null +++ b/agents/base_agent.py @@ -0,0 +1,155 @@ +""" +Base Agent module for the Agentic AGI robotics system. +All agents inherit from this base class. +""" + +import logging +import time +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Callable, Dict, List, Optional + + +class AgentStatus(Enum): + """Enumeration of possible agent statuses.""" + INITIALIZING = "initializing" + HEALTHY = "healthy" + DEGRADED = "degraded" + CRITICAL = "critical" + OFFLINE = "offline" + RECOVERING = "recovering" + + +@dataclass +class AgentState: + """Data class representing the current state of an agent.""" + name: str + status: AgentStatus = AgentStatus.INITIALIZING + last_heartbeat: float = field(default_factory=time.time) + cpu_usage: float = 0.0 + memory_usage: float = 0.0 + error_count: int = 0 + task_count: int = 0 + response_time: float = 0.0 + metadata: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class AgentMessage: + """Data class representing a message exchanged between agents.""" + sender: str + recipient: str + message_type: str + payload: Dict[str, Any] + timestamp: float = field(default_factory=time.time) + priority: int = 0 + + +class BaseAgent(ABC): + """ + Abstract base class for all agents in the Agentic AGI robotics system. + + Provides common functionality including: + - State management + - Message passing + - Health reporting + - Lifecycle management + """ + + def __init__(self, name: str, priority: int = 0): + self.name = name + self.priority = priority + self.state = AgentState(name=name) + self.logger = logging.getLogger(f"agent.{name}") + self._message_handlers: Dict[str, Callable] = {} + self._message_queue: List[AgentMessage] = [] + self._running = False + self._connected_agents: Dict[str, "BaseAgent"] = {} + + def connect_agent(self, agent: "BaseAgent") -> None: + """Register another agent for inter-agent communication.""" + self._connected_agents[agent.name] = agent + self.logger.debug("Connected to agent: %s", agent.name) + + def send_message(self, recipient: str, message_type: str, payload: Dict[str, Any], + priority: int = 0) -> bool: + """Send a message to another agent.""" + if recipient not in self._connected_agents: + self.logger.warning("Agent '%s' not found in connected agents", recipient) + return False + + msg = AgentMessage( + sender=self.name, + recipient=recipient, + message_type=message_type, + payload=payload, + priority=priority, + ) + self._connected_agents[recipient].receive_message(msg) + return True + + def receive_message(self, message: AgentMessage) -> None: + """Receive and queue a message for processing.""" + self._message_queue.append(message) + handler = self._message_handlers.get(message.message_type) + if handler: + handler(message) + else: + self.logger.debug( + "No handler for message type '%s' from '%s'", + message.message_type, + message.sender, + ) + + def register_message_handler(self, message_type: str, handler: Callable) -> None: + """Register a handler for a specific message type.""" + self._message_handlers[message_type] = handler + + def get_state(self) -> AgentState: + """Return the current agent state.""" + self.state.last_heartbeat = time.time() + return self.state + + def update_status(self, status: AgentStatus) -> None: + """Update the agent's operational status.""" + old_status = self.state.status + self.state.status = status + if old_status != status: + self.logger.info( + "Agent '%s' status changed: %s -> %s", self.name, old_status.value, status.value + ) + + def heartbeat(self) -> float: + """Record and return the current heartbeat timestamp.""" + self.state.last_heartbeat = time.time() + return self.state.last_heartbeat + + def start(self) -> None: + """Start the agent.""" + self._running = True + self.update_status(AgentStatus.HEALTHY) + self.on_start() + self.logger.info("Agent '%s' started", self.name) + + def stop(self) -> None: + """Stop the agent.""" + self._running = False + self.update_status(AgentStatus.OFFLINE) + self.on_stop() + self.logger.info("Agent '%s' stopped", self.name) + + @abstractmethod + def on_start(self) -> None: + """Called when agent starts. Override in subclasses.""" + + @abstractmethod + def on_stop(self) -> None: + """Called when agent stops. Override in subclasses.""" + + @abstractmethod + def execute(self, task: Dict[str, Any]) -> Dict[str, Any]: + """Execute a task. Override in subclasses.""" + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(name={self.name!r}, status={self.state.status.value!r})" diff --git a/agents/resolver_agent.py b/agents/resolver_agent.py new file mode 100644 index 0000000..e23930a --- /dev/null +++ b/agents/resolver_agent.py @@ -0,0 +1,257 @@ +""" +Resolver Agent - The 6th Agent: System Mediator and Health Guardian. + +Responsibilities: +- Conflict resolution between agents +- Error detection and recovery +- Deadlock detection and breaking +- Resource arbitration +- Priority management +- System health monitoring +- Fallback strategy execution +""" + +import logging +import threading +import time +from typing import Any, Dict, List, Optional + +from agents.base_agent import AgentMessage, AgentStatus, BaseAgent +from resolver.arbitrator import AgentArbitrator +from resolver.conflict_resolution import ConflictDetector, ConflictResolver +from resolver.deadlock_detector import DeadlockDetector +from resolver.error_recovery import ErrorRecoverySystem +from resolver.fallback_planner import FallbackPlanner +from resolver.health_monitor import HealthMonitor + + +class ResolverAgent(BaseAgent): + """ + The 6th Agent: System Resolver and Health Guardian. + + This agent monitors all other agents, resolves conflicts, handles errors + gracefully, and ensures the system continues operating even when problems arise. + """ + + AGENT_NAME = "resolver" + AGENT_PRIORITY = 6 # Highest priority - can override all others + + def __init__(self): + super().__init__(name=self.AGENT_NAME, priority=self.AGENT_PRIORITY) + self.conflict_detector = ConflictDetector() + self.conflict_resolver = ConflictResolver() + self.error_handler = ErrorRecoverySystem() + self.arbitrator = AgentArbitrator() + self.health_monitor = HealthMonitor() + self.deadlock_detector = DeadlockDetector() + self.fallback_planner = FallbackPlanner() + self._monitored_agents: Dict[str, BaseAgent] = {} + self._monitor_thread: Optional[threading.Thread] = None + self._monitor_interval: float = 1.0 # seconds between health checks + + # ------------------------------------------------------------------ # + # Lifecycle + # ------------------------------------------------------------------ # + + def on_start(self) -> None: + """Start background monitoring thread.""" + self._monitor_thread = threading.Thread( + target=self._monitoring_loop, daemon=True, name="resolver-monitor" + ) + self._monitor_thread.start() + self.logger.info("Resolver Agent started - monitoring %d agents", + len(self._monitored_agents)) + + def on_stop(self) -> None: + """Stop background monitoring thread.""" + if self._monitor_thread and self._monitor_thread.is_alive(): + # The daemon thread will stop with the process; signal the loop + self._running = False + self.logger.info("Resolver Agent stopped") + + # ------------------------------------------------------------------ # + # Agent integration + # ------------------------------------------------------------------ # + + def integrate_with_agents(self, agents: List[BaseAgent]) -> None: + """Connect resolver to a list of agents for monitoring and arbitration.""" + for agent in agents: + self._monitored_agents[agent.name] = agent + self.connect_agent(agent) + agent.connect_agent(self) + self.health_monitor.register_agent(agent.name) + self.logger.info("Integrated with agent: %s", agent.name) + + def start_monitoring(self) -> None: + """Start the resolver and all monitoring sub-systems.""" + if not self._running: + self.start() + + # ------------------------------------------------------------------ # + # Monitoring loop + # ------------------------------------------------------------------ # + + def _monitoring_loop(self) -> None: + """Background loop that continuously monitors all registered agents.""" + while self._running: + try: + self.monitor_agents() + time.sleep(self._monitor_interval) + except Exception as exc: # pylint: disable=broad-except + self.logger.error("Error in monitoring loop: %s", exc) + + def monitor_agents(self) -> Dict[str, Any]: + """Monitor all registered agents and return a status summary.""" + results: Dict[str, Any] = {} + for agent_name, agent in self._monitored_agents.items(): + try: + state = agent.get_state() + self.health_monitor.update_agent_health(agent_name, state) + results[agent_name] = { + "status": state.status.value, + "last_heartbeat": state.last_heartbeat, + "error_count": state.error_count, + } + except Exception as exc: # pylint: disable=broad-except + self.logger.warning("Failed to monitor agent '%s': %s", agent_name, exc) + results[agent_name] = {"status": "unreachable", "error": str(exc)} + + # Check for conflicts and deadlocks + conflicts = self.detect_conflicts() + for conflict in conflicts: + self.resolve_conflict(conflict) + + deadlock = self.deadlock_detector.detect() + if deadlock: + self.break_deadlock(deadlock) + + return results + + # ------------------------------------------------------------------ # + # Conflict management + # ------------------------------------------------------------------ # + + def detect_conflicts(self) -> List[Dict[str, Any]]: + """Detect active conflicts between agents.""" + agent_states = { + name: agent.get_state() + for name, agent in self._monitored_agents.items() + } + return self.conflict_detector.detect_all(agent_states) + + def resolve_conflict(self, conflict: Dict[str, Any]) -> Dict[str, Any]: + """Resolve a conflict between agents and return the resolution.""" + self.logger.info("Resolving conflict: %s", conflict.get("type", "unknown")) + resolution = self.conflict_resolver.resolve(conflict) + + # Notify involved agents of the resolution + for agent_name in conflict.get("agents", []): + if agent_name in self._monitored_agents: + self.send_message( + recipient=agent_name, + message_type="conflict_resolution", + payload={"conflict": conflict, "resolution": resolution}, + priority=self.AGENT_PRIORITY, + ) + return resolution + + # ------------------------------------------------------------------ # + # Error recovery + # ------------------------------------------------------------------ # + + def recover_from_error(self, error: Dict[str, Any]) -> Dict[str, Any]: + """Handle an error and attempt recovery. Returns the recovery result.""" + self.logger.warning( + "Recovering from error: type=%s, severity=%s", + error.get("type", "unknown"), + error.get("severity", "unknown"), + ) + return self.error_handler.execute_recovery(error) + + # ------------------------------------------------------------------ # + # Resource arbitration + # ------------------------------------------------------------------ # + + def arbitrate_resources(self, requests: List[Dict[str, Any]]) -> Dict[str, Any]: + """Decide resource allocation when multiple agents request the same resource.""" + return self.arbitrator.arbitrate_resource_allocation(requests) + + # ------------------------------------------------------------------ # + # Deadlock management + # ------------------------------------------------------------------ # + + def detect_deadlock(self) -> Optional[Dict[str, Any]]: + """Detect deadlocks among monitored agents.""" + return self.deadlock_detector.detect() + + def break_deadlock(self, deadlock: Dict[str, Any]) -> Dict[str, Any]: + """Break a detected deadlock.""" + self.logger.warning("Breaking deadlock: %s", deadlock) + return self.deadlock_detector.break_deadlock(deadlock) + + # ------------------------------------------------------------------ # + # Health assessment + # ------------------------------------------------------------------ # + + def assess_system_health(self) -> Dict[str, Any]: + """Check and return the overall system health status.""" + agent_health = {} + for agent_name in self._monitored_agents: + agent_health[agent_name] = self.health_monitor.get_agent_health(agent_name) + + overall = self.health_monitor.compute_overall_status(agent_health) + alerts = self.health_monitor.get_active_alerts() + + return { + "overall_status": overall, + "agents": agent_health, + "alerts": alerts, + "timestamp": time.time(), + } + + def generate_health_report(self) -> Dict[str, Any]: + """Generate a detailed health report for the entire system.""" + health = self.assess_system_health() + health["report_generated_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) + health["monitored_agents_count"] = len(self._monitored_agents) + return health + + # ------------------------------------------------------------------ # + # Fallback execution + # ------------------------------------------------------------------ # + + def execute_fallback(self, failure: Dict[str, Any]) -> Dict[str, Any]: + """Execute a fallback strategy in response to a failure.""" + self.logger.info("Executing fallback for failure: %s", failure.get("type", "unknown")) + plan = self.fallback_planner.plan_fallback(failure) + return self.fallback_planner.execute_fallback(plan) + + # ------------------------------------------------------------------ # + # Dashboard + # ------------------------------------------------------------------ # + + def launch_dashboard(self, port: int = 8080) -> None: + """Launch the health monitoring web dashboard.""" + from dashboard.health_dashboard import HealthDashboard # pylint: disable=import-outside-toplevel + dashboard = HealthDashboard(resolver_agent=self, port=port) + dashboard.run() + + # ------------------------------------------------------------------ # + # BaseAgent interface + # ------------------------------------------------------------------ # + + def execute(self, task: Dict[str, Any]) -> Dict[str, Any]: + """Execute a resolver task (dispatched by task type).""" + task_type = task.get("type", "") + dispatch: Dict[str, Any] = { + "resolve_conflict": lambda: self.resolve_conflict(task.get("conflict", {})), + "recover_error": lambda: self.recover_from_error(task.get("error", {})), + "arbitrate": lambda: self.arbitrate_resources(task.get("requests", [])), + "health_check": lambda: self.assess_system_health(), + "detect_deadlock": lambda: self.detect_deadlock(), + "execute_fallback": lambda: self.execute_fallback(task.get("failure", {})), + } + handler = dispatch.get(task_type) + if handler is None: + return {"error": f"Unknown task type: {task_type}"} + return handler() diff --git a/config/priorities.yaml b/config/priorities.yaml new file mode 100644 index 0000000..ed82f39 --- /dev/null +++ b/config/priorities.yaml @@ -0,0 +1,28 @@ +# Agent Priority Configuration +# ============================================================ +# Defines the priority level for each agent in the system. +# Higher values indicate higher precedence during arbitration +# and conflict resolution. +# +# The Resolver Agent has the highest priority (6) and can +# override any decision made by lower-priority agents. + +agent_priorities: + resolver: 6 # System Mediator – highest priority + perception: 5 # Always needs to see the environment + planning: 4 # Strategic decision-making + control: 3 # Physical robot control + communication: 2 # Human interaction + coordination: 1 # Multi-robot team management + +# Context-based priority overrides +# These rules temporarily elevate agent priority under specific conditions. +context_overrides: + emergency_stop: + control: 10 # Control agent takes absolute precedence in emergencies + + battery_critical: + planning: 8 # Planning must find charging route immediately + + human_command: + communication: 7 # Human commands override normal planning diff --git a/config/recovery_strategies.yaml b/config/recovery_strategies.yaml new file mode 100644 index 0000000..d1f793d --- /dev/null +++ b/config/recovery_strategies.yaml @@ -0,0 +1,87 @@ +# Recovery Strategies Configuration +# ============================================================ +# Defines the automatic recovery strategies for each error type +# and the fallback hierarchy for each subsystem. + +recovery_strategies: + + sensor_failure: + primary: switch_to_backup_sensor + secondary: degrade_gracefully + tertiary: request_manual_sensor_check + max_attempts: 3 + timeout_seconds: 10 + + agent_crash: + primary: restart_agent + secondary: redistribute_tasks + tertiary: request_human_intervention + max_attempts: 3 + timeout_seconds: 15 + + planning_failure: + primary: retry_with_different_algorithm + secondary: use_default_safe_plan + tertiary: request_human_plan + max_attempts: 5 + timeout_seconds: 20 + + execution_failure: + primary: replan_trajectory + secondary: try_alternative_path + tertiary: stop_and_wait + max_attempts: 3 + timeout_seconds: 30 + + communication_failure: + primary: retry_connection + secondary: switch_to_backup_channel + tertiary: failsafe_autonomous_mode + max_attempts: 5 + timeout_seconds: 5 + + hardware_failure: + primary: emergency_stop + secondary: limp_mode + tertiary: request_maintenance + max_attempts: 1 + timeout_seconds: 2 + +# Fallback hierarchy for each subsystem (ordered by preference) +fallback_hierarchy: + + navigation: + - retry_primary_navigation + - try_alternative_path + - try_simple_straight_line + - try_manual_waypoints + - request_human_guidance + - stop_safely + + manipulation: + - retry_primary_grasp + - try_alternative_grasp_approach + - try_different_grasp_point + - request_human_demonstration + - skip_object + + perception: + - retry_primary_perception + - switch_to_backup_sensor + - use_cached_perception_data + - request_human_visual_confirmation + - degrade_gracefully + + planning: + - retry_primary_planner + - try_alternative_planner + - use_default_safe_plan + - request_human_plan + - halt_and_wait + + communication: + - retry_primary_channel + - switch_to_backup_channel + - use_cached_commands + - request_local_mode + - failsafe_autonomous_mode diff --git a/config/resolver_config.yaml b/config/resolver_config.yaml new file mode 100644 index 0000000..4416a4d --- /dev/null +++ b/config/resolver_config.yaml @@ -0,0 +1,81 @@ +# Resolver Agent Configuration +# ============================================================ +# Main configuration file for the Resolver Agent (6th Agent). +# All thresholds and behaviour settings are defined here. + +resolver: + # Monitoring frequency in Hz + monitoring_rate: 10 + + # Health check frequency in Hz + health_check_rate: 1 + + # Agent priorities (higher = more important) + priorities: + perception: 5 + planning: 4 + control: 3 + communication: 2 + coordination: 1 + resolver: 6 # Highest – can override all other agents + + conflict_resolution: + # Default strategy when no other context is available + # Options: priority_based | voting | expertise | cost_based | ml_based + default_strategy: priority_based + + # Enable ML-based conflict prediction (requires PyTorch) + use_ml_prediction: true + + # Minimum fraction of votes required for voting strategy + voting_threshold: 0.6 + + error_recovery: + # Automatically attempt recovery without human intervention + auto_recovery: true + + # Maximum number of recovery attempts before escalating + max_recovery_attempts: 3 + + # Seconds before escalating an unresolved error to a human operator + escalation_timeout: 30 + + health_monitoring: + # Agent response time above this (seconds) triggers a DEGRADED alert + response_time_threshold: 1.0 + + # CPU usage above this percentage triggers a CRITICAL alert + cpu_threshold: 80 + + # Memory usage above this percentage triggers a CRITICAL alert + memory_threshold: 85 + + # Error rate above this fraction (0.1 = 10 %) triggers a WARNING alert + error_rate_threshold: 0.1 + + # Seconds without a heartbeat before marking an agent as OFFLINE + heartbeat_timeout: 30 + + deadlock_detection: + # How often to run deadlock detection (seconds) + check_interval: 5 + + # Seconds a wait may last before being flagged as a potential deadlock + timeout_threshold: 30 + + fallback: + # Enable the fallback strategy system + enable_fallback: true + + # Maximum fallback depth (0 = primary, 3 = safe shutdown) + max_fallback_depth: 3 + + # Request human help before performing a safe shutdown + request_human_help: true + + dashboard: + # Default port for the health monitoring web dashboard + port: 8080 + + # How often the dashboard data is refreshed (seconds) + refresh_interval: 2 diff --git a/dashboard/__init__.py b/dashboard/__init__.py new file mode 100644 index 0000000..939da66 --- /dev/null +++ b/dashboard/__init__.py @@ -0,0 +1,7 @@ +""" +Dashboard package for the Resolver Agent health monitoring UI. +""" + +from dashboard.health_dashboard import HealthDashboard + +__all__ = ["HealthDashboard"] diff --git a/dashboard/health_dashboard.py b/dashboard/health_dashboard.py new file mode 100644 index 0000000..9d0ac85 --- /dev/null +++ b/dashboard/health_dashboard.py @@ -0,0 +1,130 @@ +""" +Web-based health monitoring dashboard for the Resolver Agent. + +Provides a real-time view of all agent statuses, active alerts, +and system health metrics via a simple Flask web server. + +Dependencies: flask (optional; falls back to a stub when unavailable) +""" + +import json +import logging +import threading +import time +from typing import Any, Dict, Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from agents.resolver_agent import ResolverAgent + +logger = logging.getLogger(__name__) + +try: + from flask import Flask, Response, jsonify, render_template + + _FLASK_AVAILABLE = True +except ImportError: + _FLASK_AVAILABLE = False + logger.warning("Flask not available; HealthDashboard will run in stub mode") + + +class HealthDashboard: + """ + Real-time health monitoring dashboard. + + When Flask is available the dashboard serves: + GET / → HTML dashboard + GET /api/health → JSON health data + GET /api/agents → JSON per-agent data + GET /api/alerts → JSON active alerts + + When Flask is not installed the dashboard logs health data to the console. + """ + + def __init__(self, resolver_agent: "ResolverAgent", port: int = 8080): + self.resolver = resolver_agent + self.port = port + self._app: Optional[Any] = None + self._server_thread: Optional[threading.Thread] = None + + if _FLASK_AVAILABLE: + self._app = self._create_flask_app() + + # ------------------------------------------------------------------ # + # Public API + # ------------------------------------------------------------------ # + + def run(self, debug: bool = False) -> None: + """Start the dashboard server (blocking when Flask is available).""" + if _FLASK_AVAILABLE and self._app is not None: + logger.info("Launching HealthDashboard on http://localhost:%d", self.port) + self._app.run(host="0.0.0.0", port=self.port, debug=debug) + else: + logger.info("Flask unavailable – printing health data every 5 s") + self._console_loop() + + def run_async(self) -> threading.Thread: + """Start the dashboard server in a background daemon thread.""" + self._server_thread = threading.Thread( + target=self.run, daemon=True, name="health-dashboard" + ) + self._server_thread.start() + return self._server_thread + + def get_health_data(self) -> Dict[str, Any]: + """Return the current health data as a plain dict.""" + return self.resolver.assess_system_health() + + # ------------------------------------------------------------------ # + # Flask application + # ------------------------------------------------------------------ # + + def _create_flask_app(self) -> Any: + """Create and configure the Flask application.""" + app = Flask(__name__, template_folder="templates", static_folder="static") + + @app.route("/") + def index(): + health = self.get_health_data() + return render_template("dashboard.html", health=health) + + @app.route("/api/health") + def api_health(): + return jsonify(self.get_health_data()) + + @app.route("/api/agents") + def api_agents(): + health = self.get_health_data() + return jsonify(health.get("agents", {})) + + @app.route("/api/alerts") + def api_alerts(): + health = self.get_health_data() + return jsonify(health.get("alerts", [])) + + @app.route("/api/stream") + def api_stream(): + """Server-Sent Events endpoint for real-time updates.""" + def generate(): + while True: + data = json.dumps(self.get_health_data()) + yield f"data: {data}\n\n" + time.sleep(2) + + return Response(generate(), mimetype="text/event-stream") + + return app + + # ------------------------------------------------------------------ # + # Console fallback + # ------------------------------------------------------------------ # + + def _console_loop(self) -> None: + """Print health data to the console when Flask is unavailable.""" + while True: + health = self.get_health_data() + logger.info("System health: %s", health.get("overall_status", "unknown")) + for name, data in health.get("agents", {}).items(): + logger.info(" Agent '%s': %s", name, data.get("status", "unknown")) + for alert in health.get("alerts", []): + logger.warning(" ALERT: %s", alert.get("message", "")) + time.sleep(5) diff --git a/dashboard/static/css/dashboard.css b/dashboard/static/css/dashboard.css new file mode 100644 index 0000000..a5c3d5e --- /dev/null +++ b/dashboard/static/css/dashboard.css @@ -0,0 +1,84 @@ +/* Resolver Agent Health Dashboard Styles */ + +:root { + --healthy: #2ecc71; + --degraded: #f39c12; + --critical: #e74c3c; + --unknown: #95a5a6; + --bg: #1a1a2e; + --surface: #16213e; + --text: #eaeaea; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } + +body { + background: var(--bg); + color: var(--text); + font-family: 'Segoe UI', sans-serif; + min-height: 100vh; +} + +header { + background: var(--surface); + padding: 1rem 2rem; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 2px solid #0f3460; +} + +header h1 { font-size: 1.4rem; } +#last-updated { font-size: 0.8rem; color: #aaa; } + +main { padding: 2rem; } + +section { margin-bottom: 2rem; } +section h2 { margin-bottom: 1rem; font-size: 1.1rem; color: #aaa; } + +/* Status badge */ +.badge { + display: inline-block; + padding: 0.4rem 1.2rem; + border-radius: 999px; + font-weight: bold; + font-size: 1rem; + text-transform: uppercase; +} +.badge.healthy { background: var(--healthy); color: #fff; } +.badge.degraded { background: var(--degraded); color: #fff; } +.badge.critical { background: var(--critical); color: #fff; } +.badge.unknown { background: var(--unknown); color: #fff; } + +/* Agent cards */ +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1rem; +} + +.agent-card { + background: var(--surface); + border-radius: 8px; + padding: 1rem; + border-left: 4px solid var(--unknown); +} +.agent-card.healthy { border-left-color: var(--healthy); } +.agent-card.degraded { border-left-color: var(--degraded); } +.agent-card.critical { border-left-color: var(--critical); } + +.agent-card h3 { font-size: 0.95rem; margin-bottom: 0.5rem; text-transform: capitalize; } +.agent-card .metric { font-size: 0.8rem; color: #aaa; margin: 2px 0; } +.agent-card .metric span { color: var(--text); font-weight: bold; } + +/* Alerts */ +#alerts-list { list-style: none; } +#alerts-list li { + background: var(--surface); + border-left: 4px solid var(--critical); + padding: 0.7rem 1rem; + margin-bottom: 0.5rem; + border-radius: 4px; + font-size: 0.9rem; +} +.no-alerts { border-left-color: var(--healthy) !important; color: #aaa; } diff --git a/dashboard/static/js/dashboard.js b/dashboard/static/js/dashboard.js new file mode 100644 index 0000000..8c72e91 --- /dev/null +++ b/dashboard/static/js/dashboard.js @@ -0,0 +1,87 @@ +/* Resolver Agent Health Dashboard – real-time update script */ + +const STATUS_BADGE = document.getElementById('status-badge'); +const AGENT_CARDS = document.getElementById('agent-cards'); +const ALERTS_LIST = document.getElementById('alerts-list'); +const LAST_UPDATED = document.getElementById('last-updated'); + +function statusClass(status) { + const s = (status || 'unknown').toLowerCase(); + if (s === 'healthy') return 'healthy'; + if (s === 'degraded') return 'degraded'; + if (s === 'critical') return 'critical'; + return 'unknown'; +} + +function renderHealth(data) { + // Overall status + const overall = (data.overall_status || 'unknown').toUpperCase(); + STATUS_BADGE.textContent = overall; + STATUS_BADGE.className = 'badge ' + statusClass(data.overall_status); + + // Agent cards + AGENT_CARDS.innerHTML = ''; + const agents = data.agents || {}; + Object.entries(agents).forEach(([name, info]) => { + const sc = statusClass(info.status); + const card = document.createElement('div'); + card.className = 'agent-card ' + sc; + card.innerHTML = ` +

${name}

+

Status: ${(info.status || 'unknown').toUpperCase()}

+

CPU: ${(info.cpu_usage || 0).toFixed(1)}%

+

Memory: ${(info.memory_usage || 0).toFixed(1)}%

+

Error rate: ${((info.error_rate || 0) * 100).toFixed(1)}%

+

Response: ${(info.response_time || 0).toFixed(2)}s

+ `; + AGENT_CARDS.appendChild(card); + }); + + // Alerts + const alerts = data.alerts || []; + ALERTS_LIST.innerHTML = ''; + if (alerts.length === 0) { + ALERTS_LIST.innerHTML = '
  • No active alerts ✓
  • '; + } else { + alerts.forEach(alert => { + const li = document.createElement('li'); + li.textContent = alert.message || JSON.stringify(alert); + ALERTS_LIST.appendChild(li); + }); + } + + LAST_UPDATED.textContent = 'Last updated: ' + new Date().toLocaleTimeString(); +} + +// Use Server-Sent Events for real-time updates +function connectSSE() { + const source = new EventSource('/api/stream'); + source.onmessage = (event) => { + try { + renderHealth(JSON.parse(event.data)); + } catch (e) { + console.error('Failed to parse health data', e); + } + }; + source.onerror = () => { + console.warn('SSE disconnected; retrying via polling'); + source.close(); + pollHealth(); + }; +} + +// Fallback: poll every 5 s when SSE is unavailable +function pollHealth() { + setInterval(() => { + fetch('/api/health') + .then(r => r.json()) + .then(renderHealth) + .catch(e => console.error('Health poll failed', e)); + }, 5000); +} + +// Attempt initial fetch immediately, then start SSE +fetch('/api/health') + .then(r => r.json()) + .then(data => { renderHealth(data); connectSSE(); }) + .catch(() => pollHealth()); diff --git a/dashboard/templates/dashboard.html b/dashboard/templates/dashboard.html new file mode 100644 index 0000000..fec74e9 --- /dev/null +++ b/dashboard/templates/dashboard.html @@ -0,0 +1,37 @@ + + + + + + Resolver Agent – Health Dashboard + + + +
    +

    🤖 Resolver Agent – System Health Dashboard

    + Updating… +
    + +
    + +
    +

    Overall System Status

    +
    UNKNOWN
    +
    + + +
    +

    Agent Health

    +
    +
    + + +
    +

    Active Alerts

    +
    • No active alerts
    +
    +
    + + + + diff --git a/docs/conflict_resolution_guide.md b/docs/conflict_resolution_guide.md new file mode 100644 index 0000000..36e4e2e --- /dev/null +++ b/docs/conflict_resolution_guide.md @@ -0,0 +1,97 @@ +# Conflict Resolution Guide + +## Overview + +The conflict resolution system detects and resolves disagreements between agents in the Agentic AGI robotics system. + +## Conflict Types + +| Type | Description | +|------|-------------| +| `perception_planning` | Perception sees objects that Planning doesn't know about (or vice versa) | +| `control` | Multiple agents simultaneously request robot control | +| `task` | The same task is assigned to multiple agents | +| `resource` | Multiple agents compete for the same resource (GPU, camera, etc.) | + +## Resolution Strategies + +### 1. Priority-Based (Default) +The agent with the highest priority wins. Uses the global priority table. + +```python +from resolver.conflict_resolution import ConflictResolver, ResolutionStrategy + +resolver = ConflictResolver() +result = resolver.resolve(conflict, ResolutionStrategy.PRIORITY_BASED) +``` + +### 2. Voting +Each agent casts a weighted vote (based on priority). The agent with the most votes wins. + +### 3. Expertise +The most domain-relevant agent is selected as the decision-maker. + +| Conflict Type | Expert Agent | +|---------------------|---------------| +| perception_planning | perception | +| control | control | +| task | coordination | +| resource | resolver | + +### 4. Cost-Based +The option with the lowest numerical cost (from `conflict.details.options`) is selected. + +```python +conflict = { + "type": "resource", + "agents": ["planning", "control"], + "details": { + "options": [ + {"id": "opt_a", "agent": "planning", "cost": 10}, + {"id": "opt_b", "agent": "control", "cost": 5}, + ] + }, +} +result = resolver.resolve(conflict, ResolutionStrategy.COST_BASED) +# → control wins (cost 5) +``` + +### 5. ML-Based +A trained PyTorch model predicts the best resolution. Falls back to priority-based if PyTorch is unavailable. + +## Conflict Prediction + +The `ConflictPredictor` class provides rule-based and ML-based prediction: + +```python +from resolver.conflict_resolution import ConflictPredictor + +predictor = ConflictPredictor() +predictions = predictor.predict(agent_states) +actions = predictor.suggest_preventive_actions(agent_states) +``` + +## Conflict Data Format + +```python +conflict = { + "type": "control", # ConflictType value + "agents": ["planning", "control"], # involved agent names + "description": "...", # human-readable description + "severity": 2, # 1-5 (1=minor, 5=critical) + "details": { ... }, # type-specific details +} +``` + +## Resolution Result Format + +```python +resolution = { + "strategy": "priority_based", + "winning_agent": "planning", + "action": "grant_control_to_planning", + "rationale": "Agent 'planning' has highest priority (4)", + "success": True, + "timestamp": 1234567890.0, +} +``` diff --git a/docs/error_recovery_guide.md b/docs/error_recovery_guide.md new file mode 100644 index 0000000..c5f66c1 --- /dev/null +++ b/docs/error_recovery_guide.md @@ -0,0 +1,80 @@ +# Error Recovery Guide + +## Overview + +The error recovery system automatically detects, classifies, and recovers from errors across all robot systems. + +## Error Severity Levels + +| Level | Value | Meaning | +|----------|-------|---------| +| INFO | 0 | Minor issue; log only | +| WARNING | 1 | Potential problem; monitor | +| ERROR | 2 | Significant issue; needs recovery | +| CRITICAL | 3 | System-threatening; immediate action | +| FATAL | 4 | System shutdown required | + +## Supported Error Types + +| Type | Trigger | Recovery Action | +|------------------------|-------------------------------|-----------------| +| `sensor_failure` | Camera/LIDAR offline | Switch to backup sensor | +| `agent_crash` | Agent stops responding (30s) | Restart agent | +| `planning_failure` | No valid plan found | Try alternative planner | +| `execution_failure` | Robot cannot reach goal | Re-plan trajectory | +| `communication_failure`| Connection lost | Use cached data / fail-safe | +| `hardware_failure` | Motor error / battery low | Emergency stop / limp mode | + +## Usage + +```python +from resolver.error_recovery import ErrorRecoverySystem, ErrorSeverity, ErrorType + +system = ErrorRecoverySystem() + +# Recover from an error +error = { + "type": ErrorType.SENSOR_FAILURE, + "sensor": "camera", + "severity": ErrorSeverity.ERROR, +} +result = system.execute_recovery(error) +# result["recovery_action"] → "switched_to_backup_sensor:camera" +# result["recovery_success"] → True +``` + +## Escalation + +After `MAX_RECOVERY_ATTEMPTS` (default: 3) failed attempts for the same error, the system escalates: + +```python +result["escalation_required"] = True # Human intervention needed +``` + +## Integration with Resolver Agent + +```python +resolver = ResolverAgent() +resolver.recover_from_error({ + "type": "agent_crash", + "agent": "planning", + "severity": 3, +}) +``` + +## Error Log + +```python +print(system.error_log) # List of all ErrorRecord dicts +``` + +## Detection + +The system can auto-detect errors by scanning agent states: + +```python +errors = system.detect_errors(agent_states) +# Returns list of error dicts for agents with: +# - No heartbeat for >30 seconds (agent_crash) +# - Error count >10 (unknown) +``` diff --git a/docs/resolver_agent.md b/docs/resolver_agent.md new file mode 100644 index 0000000..d3a1f7b --- /dev/null +++ b/docs/resolver_agent.md @@ -0,0 +1,114 @@ +# Resolver Agent + +## Overview + +The **Resolver Agent** is the 6th intelligent agent in the Agentic AGI robotics system. It acts as the **System Mediator and Health Guardian**, monitoring all other agents, resolving conflicts, handling errors gracefully, and ensuring the system continues operating even when problems arise. + +## Architecture + +``` +ResolverAgent +├── ConflictDetector ← detects disagreements between agents +├── ConflictResolver ← applies resolution strategies +├── ErrorRecoverySystem ← handles errors and automatic recovery +├── AgentArbitrator ← allocates resources and arbitrates control +├── HealthMonitor ← tracks metrics and raises alerts +├── DeadlockDetector ← finds and breaks deadlocks +└── FallbackPlanner ← executes fallback strategies +``` + +## Responsibilities + +| Responsibility | Module | +|-----------------------|---------------------------| +| Conflict resolution | `resolver/conflict_resolution.py` | +| Error recovery | `resolver/error_recovery.py` | +| Agent arbitration | `resolver/arbitrator.py` | +| Health monitoring | `resolver/health_monitor.py` | +| Deadlock detection | `resolver/deadlock_detector.py` | +| Fallback planning | `resolver/fallback_planner.py` | +| ML conflict prediction| `resolver/predictor.py` | + +## Quick Start + +```python +from agents.resolver_agent import ResolverAgent +from agents.base_agent import BaseAgent + +# Create the resolver +resolver = ResolverAgent() + +# Connect to other agents +resolver.integrate_with_agents([perception, planning, control, + communication, coordination]) + +# Start monitoring (non-blocking background thread) +resolver.start_monitoring() + +# Manually resolve a conflict +conflict = { + "type": "control", + "agents": ["planning", "control"], + "description": "Path disagreement", +} +resolution = resolver.resolve_conflict(conflict) + +# Check system health +health = resolver.generate_health_report() + +# Handle an error +error = {"type": "sensor_failure", "sensor": "camera", "severity": 2} +result = resolver.recover_from_error(error) +``` + +## Agent Priorities + +| Agent | Priority | +|---------------|----------| +| resolver | 6 (highest) | +| perception | 5 | +| planning | 4 | +| control | 3 | +| communication | 2 | +| coordination | 1 (lowest) | + +## Configuration + +See [`config/resolver_config.yaml`](../config/resolver_config.yaml) for all configuration options. + +## API Reference + +### `ResolverAgent` + +| Method | Description | +|--------|-------------| +| `integrate_with_agents(agents)` | Connect resolver to a list of agents | +| `start_monitoring()` | Start background monitoring loop | +| `monitor_agents()` | Run one monitoring cycle, return status dict | +| `detect_conflicts()` | Return list of active conflicts | +| `resolve_conflict(conflict)` | Resolve a conflict dict | +| `recover_from_error(error)` | Handle an error and attempt recovery | +| `arbitrate_resources(requests)` | Decide resource allocation | +| `detect_deadlock()` | Check for deadlocks (returns dict or None) | +| `break_deadlock(deadlock)` | Break a detected deadlock | +| `assess_system_health()` | Return overall system health | +| `generate_health_report()` | Return detailed health report | +| `execute_fallback(failure)` | Execute fallback strategy | +| `launch_dashboard(port=8080)` | Start the web health dashboard | + +## ROS2 Integration + +See [`ros2_interface/resolver_node.py`](../ros2_interface/resolver_node.py) for the ROS2 node implementation. + +Launch with: +```bash +ros2 launch rag7 resolver.launch.py +``` + +## Testing + +```bash +python -m pytest tests/ -v +``` + +All 87+ tests should pass. diff --git a/examples/conflict_resolution_demo.py b/examples/conflict_resolution_demo.py new file mode 100644 index 0000000..b420c84 --- /dev/null +++ b/examples/conflict_resolution_demo.py @@ -0,0 +1,120 @@ +""" +Conflict Resolution Demo + +Demonstrates the Resolver Agent's conflict detection and resolution +capabilities with multiple resolution strategies. +""" + +import sys +import os +import logging + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s") + +from agents.base_agent import AgentState, AgentStatus +from agents.resolver_agent import ResolverAgent +from resolver.conflict_resolution import ConflictDetector, ConflictResolver, ResolutionStrategy + + +def demo_perception_planning_conflict(): + print("\n=== Perception vs Planning Conflict ===") + detector = ConflictDetector() + state_p = AgentState(name="perception") + state_p.metadata["detected_objects"] = ["chair", "table", "robot"] + state_pl = AgentState(name="planning") + state_pl.metadata["known_objects"] = ["chair"] + + conflicts = detector.detect_perception_planning_conflict( + {"perception": state_p, "planning": state_pl} + ) + print(f"Detected {len(conflicts)} conflict(s):") + for c in conflicts: + print(f" Type: {c['type']}, Severity: {c['severity']}") + print(f" Missing objects: {c['details']['disagreements']}") + return conflicts + + +def demo_resource_conflict(): + print("\n=== Resource Conflict (Multiple Agents Need GPU) ===") + detector = ConflictDetector() + states = {} + for name in ["perception", "planning", "control"]: + state = AgentState(name=name) + state.metadata["requested_resources"] = ["GPU"] + states[name] = state + + conflicts = detector.detect_resource_conflict(states) + print(f"Detected {len(conflicts)} resource conflict(s):") + for c in conflicts: + print(f" Resource: {c['details']['resource']}") + print(f" Requesters: {c['details']['requesting_agents']}") + return conflicts + + +def demo_resolution_strategies(): + print("\n=== Resolution Strategies Demo ===") + resolver = ConflictResolver() + conflict = { + "type": "control", + "agents": ["planning", "control", "coordination"], + "description": "Multiple agents requesting robot control", + } + + strategies = [ + ResolutionStrategy.PRIORITY_BASED, + ResolutionStrategy.VOTING, + ResolutionStrategy.EXPERTISE, + ] + + for strategy in strategies: + result = resolver.resolve(conflict, strategy) + print(f" {strategy.value:20s} → winner: {result['winning_agent']}") + + +def demo_full_resolver(): + print("\n=== Full Resolver Agent Demo ===") + from agents.base_agent import BaseAgent + + class _DummyAgent(BaseAgent): + def __init__(self, name, priority=0): + super().__init__(name, priority) + def on_start(self): pass + def on_stop(self): pass + def execute(self, task): return {} + + resolver = ResolverAgent() + agents = [ + _DummyAgent("perception", 5), + _DummyAgent("planning", 4), + _DummyAgent("control", 3), + _DummyAgent("communication", 2), + _DummyAgent("coordination", 1), + ] + resolver.integrate_with_agents(agents) + resolver.start() + + # Simulate a control conflict + conflict = { + "type": "control", + "agents": ["planning", "control"], + "description": "Path disagreement", + } + print(f"\nResolving conflict: {conflict['description']}") + resolution = resolver.resolve_conflict(conflict) + print(f" Resolution: {resolution['winning_agent']} granted control " + f"({resolution['strategy']})") + + # Check system health + health = resolver.generate_health_report() + print(f"\nSystem health: {health['overall_status']}") + + resolver.stop() + print("\nDemo completed successfully!") + + +if __name__ == "__main__": + demo_perception_planning_conflict() + demo_resource_conflict() + demo_resolution_strategies() + demo_full_resolver() diff --git a/examples/error_recovery_demo.py b/examples/error_recovery_demo.py new file mode 100644 index 0000000..d547c00 --- /dev/null +++ b/examples/error_recovery_demo.py @@ -0,0 +1,87 @@ +""" +Error Recovery Demo + +Demonstrates the Resolver Agent's error detection and automatic +recovery capabilities for various error types. +""" + +import sys +import os +import logging + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s") + +from resolver.error_recovery import ErrorRecoverySystem, ErrorSeverity, ErrorType + + +def demo_error_classification(): + print("\n=== Error Classification ===") + system = ErrorRecoverySystem() + errors = [ + {"type": ErrorType.SENSOR_FAILURE, "sensor": "camera", "severity": ErrorSeverity.ERROR}, + {"type": ErrorType.AGENT_CRASH, "agent": "planning", "severity": ErrorSeverity.CRITICAL}, + {"type": ErrorType.PLANNING_FAILURE, "severity": ErrorSeverity.ERROR}, + {"type": ErrorType.HARDWARE_FAILURE, "component": "motor", "severity": ErrorSeverity.CRITICAL}, + ] + for error in errors: + record = system.classify_error(error) + print(f" {record.error_type:30s} → severity: {record.severity.name}") + + +def demo_automatic_recovery(): + print("\n=== Automatic Error Recovery ===") + system = ErrorRecoverySystem() + scenarios = [ + { + "name": "Camera failure during navigation", + "error": {"type": ErrorType.SENSOR_FAILURE, "sensor": "camera", + "severity": ErrorSeverity.ERROR}, + }, + { + "name": "Planning agent becomes unresponsive", + "error": {"type": ErrorType.AGENT_CRASH, "agent": "planning", + "severity": ErrorSeverity.CRITICAL}, + }, + { + "name": "Robot cannot reach goal", + "error": {"type": ErrorType.EXECUTION_FAILURE, + "severity": ErrorSeverity.ERROR}, + }, + { + "name": "Communication link lost", + "error": {"type": ErrorType.COMMUNICATION_FAILURE, + "severity": ErrorSeverity.WARNING}, + }, + { + "name": "Motor controller error", + "error": {"type": ErrorType.HARDWARE_FAILURE, "component": "motor", + "severity": ErrorSeverity.CRITICAL}, + }, + ] + + for scenario in scenarios: + print(f"\n Scenario: {scenario['name']}") + result = system.execute_recovery(scenario["error"]) + print(f" Action: {result['recovery_action']}") + print(f" Success: {result['recovery_success']}") + + +def demo_escalation(): + print("\n=== Recovery Escalation (max attempts exceeded) ===") + system = ErrorRecoverySystem() + error = {"type": ErrorType.SENSOR_FAILURE, "sensor": "lidar"} + + for attempt in range(ErrorRecoverySystem.MAX_RECOVERY_ATTEMPTS + 1): + result = system.execute_recovery(error) + if result.get("escalation_required"): + print(f" Attempt {attempt + 1}: ESCALATED – human intervention required") + else: + print(f" Attempt {attempt + 1}: Recovery action → {result['recovery_action']}") + + +if __name__ == "__main__": + demo_error_classification() + demo_automatic_recovery() + demo_escalation() + print("\nDemo completed successfully!") diff --git a/examples/health_monitoring_demo.py b/examples/health_monitoring_demo.py new file mode 100644 index 0000000..2550968 --- /dev/null +++ b/examples/health_monitoring_demo.py @@ -0,0 +1,108 @@ +""" +Health Monitoring Demo + +Demonstrates the Resolver Agent's health monitoring capabilities +including per-agent metrics, alerts, and failure prediction. +""" + +import sys +import os +import logging + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s") + +from agents.base_agent import AgentState, AgentStatus, BaseAgent +from agents.resolver_agent import ResolverAgent +from resolver.health_monitor import HealthMonitor + + +def demo_individual_agent_monitoring(): + print("\n=== Individual Agent Health Monitoring ===") + monitor = HealthMonitor() + + # Simulate different agent health scenarios + scenarios = { + "perception": {"cpu_usage": 20.0, "memory_usage": 30.0, + "response_time": 0.05, "error_count": 0, "task_count": 100}, + "planning": {"cpu_usage": 45.0, "memory_usage": 60.0, + "response_time": 1.8, "error_count": 2, "task_count": 50}, + "control": {"cpu_usage": 85.0, "memory_usage": 40.0, + "response_time": 0.1, "error_count": 0, "task_count": 200}, + "communication": {"cpu_usage": 15.0, "memory_usage": 20.0, + "response_time": 0.2, "error_count": 8, "task_count": 80}, + } + + for agent_name, metrics in scenarios.items(): + state = AgentState(name=agent_name) + for k, v in metrics.items(): + setattr(state, k, v) + monitor.update_agent_health(agent_name, state) + health = monitor.get_agent_health(agent_name) + print(f" {agent_name:15s}: {health['status'].upper():10s} " + f"CPU={health['cpu_usage']:5.1f}% " + f"RT={health['response_time']:.2f}s") + + +def demo_system_health_report(): + print("\n=== System Health Report ===") + monitor = HealthMonitor() + + for agent_name, (cpu, rt) in [ + ("perception", (20.0, 0.1)), + ("planning", (50.0, 0.5)), + ("control", (90.0, 0.05)), # Critical: high CPU + ]: + state = AgentState(name=agent_name) + state.cpu_usage = cpu + state.response_time = rt + monitor.update_agent_health(agent_name, state) + + report = monitor.generate_health_report() + print(f" Overall status: {report['overall_status'].upper()}") + print(f" Monitored agents: {report['total_agents']}") + print(f" Active alerts: {len(report['alerts'])}") + for alert in report["alerts"]: + print(f" ⚠️ {alert['message']}") + + +def demo_full_resolver_health(): + print("\n=== Resolver Agent Health Dashboard Data ===") + + class _DummyAgent(BaseAgent): + def __init__(self, name, priority=0): + super().__init__(name, priority) + def on_start(self): pass + def on_stop(self): pass + def execute(self, task): return {} + + resolver = ResolverAgent() + agents = [ + _DummyAgent("perception", 5), + _DummyAgent("planning", 4), + _DummyAgent("control", 3), + _DummyAgent("communication", 2), + _DummyAgent("coordination", 1), + ] + for agent in agents: + agent.start() + resolver.integrate_with_agents(agents) + resolver.start() + + health = resolver.generate_health_report() + print(f" Overall: {health['overall_status'].upper()}") + print(f" Generated: {health['report_generated_at']}") + print(" Agents:") + for name, data in health["agents"].items(): + print(f" {name:15s}: {data.get('status', 'unknown').upper()}") + + resolver.stop() + for agent in agents: + agent.stop() + print("\nDemo completed successfully!") + + +if __name__ == "__main__": + demo_individual_agent_monitoring() + demo_system_health_report() + demo_full_resolver_health() diff --git a/resolver/__init__.py b/resolver/__init__.py new file mode 100644 index 0000000..5cd5be6 --- /dev/null +++ b/resolver/__init__.py @@ -0,0 +1,28 @@ +""" +Resolver package for the Agentic AGI robotics system. + +Provides conflict resolution, error recovery, agent arbitration, +health monitoring, deadlock detection, fallback planning, and +ML-based conflict prediction. +""" + +from resolver.arbitrator import AgentArbitrator +from resolver.conflict_resolution import ConflictDetector, ConflictPredictor, ConflictResolver +from resolver.deadlock_detector import DeadlockDetector +from resolver.error_recovery import ErrorRecoverySystem, ErrorSeverity +from resolver.fallback_planner import FallbackPlanner +from resolver.health_monitor import HealthMonitor +from resolver.predictor import ConflictPredictor as MLConflictPredictor + +__all__ = [ + "ConflictDetector", + "ConflictResolver", + "ConflictPredictor", + "MLConflictPredictor", + "ErrorRecoverySystem", + "ErrorSeverity", + "AgentArbitrator", + "HealthMonitor", + "DeadlockDetector", + "FallbackPlanner", +] diff --git a/resolver/__pycache__/__init__.cpython-312.pyc b/resolver/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba09822a25890b06519c5cb3e201278c9c7baa9b GIT binary patch literal 1006 zcmZWnJ#W-N5Z(2C#+M_PToDB!wC#y-h5|)M2q_{YASEbDIu}{_?9H8(UGHhV2|7A{ zMMO)-FQK5KpjqjZf{G4^Xi_n|wl7Fn@_2SU`{vEO`O@wB2%h_&FVhb-gudHD^?0Ro zJq70yB`8D*PCSl<7kb$GHC_|-ur3;5Lo~yti`98cw8OUZ8+=Llp)WdN$DNyeS#-m$ z^IQCmSP560-{w8QSjA|JlBJJ0T$}lW&bx2^5z8gNV2Vtm_$(STaw-+k6GpbjEYoRB zw)b8WB}WqcoaD1yGcol2LnSZLgykfb*(pzBO_T*X*Qv}l2~!GC6pJMw&o&9*W|~B5 zlxh{Z6o0}Zt|vsuEY(t_*?5yAEK0bH&%n_vE|5+m&PTwEOnH=LE-lIu|6u>&D9TxK zTYjoo0yLof(EnklI>2Vd^{`!*m1}F%SE_lqK^Hk3)i`}w^L)!vV5bC~i-5(Uq53p)j!oI4*M>Van0n-Lf}>zG+dK$e2>ocai{)VKsTF+$D2w@u!lH-pW_7uml_x zb9a$k2}BEl0L>OQ>x%@ZJ3tk>-%>RjiI8)gmi9jo#a_^IFI;ZoSLoy`I+>%bE3`XDPv+?H9PRzCcRJqtmRg7W EzY(K3mjD0& literal 0 HcmV?d00001 diff --git a/resolver/__pycache__/arbitrator.cpython-312.pyc b/resolver/__pycache__/arbitrator.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d66a41faf155d6720a38c07d60c7b9fc5230616 GIT binary patch literal 9829 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 literal 0 HcmV?d00001 diff --git a/resolver/__pycache__/conflict_resolution.cpython-312.pyc b/resolver/__pycache__/conflict_resolution.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d2406f20a7e5703691a9057f7c2021855234ff5 GIT binary patch literal 19103 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

    In#s_$`F9aw|Ny%p<|%PGhlj zsXIs(9K8b5NVyWZEt9+%nIp%L#E~R`D7f^;;OU_096Svjspeg9Si!+%T!-o4|3J;M zx$XY>jHR%*Im13}5$<2j@I}^TZTO3Dk%FHur~W4S&AA72YU|YG!UXpNlwnmVeC7NWFHd5R-JEMc!dN#gt7;K=N9#xv2Z*H zw6SHK^}T>kPDhJ-8Ezn}6e;*w^R9(|-AFDUB{#7L-ffKjfPQY`xf6wkrXu<6I=B*k z@R5Ff>ii?|Uk3guqMmw>bUd>t~-sP>%Qq@b_Z(Zub^CRfg_dRB%X zd{(63cCB+wd30{=#;*gLcw0P4nQMv+!@ZduTJf%gR|i(69=58T$6vrpyt!#IMvLxd z&b$0UR$Li)5K&tWy`b}CtV@cF@rC~V&huJ@@-9-`n+|oYTdhhJZJ5k$AFBnf5*6J$XJ2En=|$z-hS8l N9z%OyP)NxO|1Vqt_NxE@ literal 0 HcmV?d00001 diff --git a/resolver/__pycache__/error_recovery.cpython-312.pyc b/resolver/__pycache__/error_recovery.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5e1030836a69c11bbe11920243ec63a0a4fe56fa GIT binary patch literal 11129 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 X_ieD(-Oo^bxqFi5PCp_jnI--kUHgW7 literal 0 HcmV?d00001 diff --git a/resolver/__pycache__/health_monitor.cpython-312.pyc b/resolver/__pycache__/health_monitor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6c0125111f62f20b5c4449bb8a33842212b7c84 GIT binary patch literal 12487 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 Biwgh% literal 0 HcmV?d00001 diff --git a/resolver/__pycache__/predictor.cpython-312.pyc b/resolver/__pycache__/predictor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d63370922860ba1b49f947d2e84b8c6be15cd44 GIT binary patch literal 10653 zcmbVSdu$u`eZM2`_|j37Eb9F_M{;7?4*j+pJGLxak|SG|A5zjpX~WRGlSG*h_3mg} z1l75KyHcIISo1o`ZHCfWQQLFSDt}c4hRp^FG)0Gj43&w!s+fna>kJGS9i>PR?2moF zKi(ZFiD|c;px^zT-{0fD-_Q5)<1d^}2L;b}e>N9nHdEAZu%JJzmRbEz$jng!H9`rr zU<%PAGtnkrMl9#7sMGWI2 zlwc$V%ZHUkCq)Tn^g5p4sSSD<)!ZGKQkfpuzRC;)Wkt1}om7I+h(9#k7wc9 zedjhlKks^i;P3>|$75GnIR^Z#+I+rXB&hg&swomt9Yf+*Vjycle@L|q1S6tfQke_l zz{{$&M~Y5HV~T1W3XX-NK|!@@LJV@j$YN+zvIAiW4_&qUe1VW(mVG{%filnY*FHP6 zb37UrcS^BHM3i=-^zW4XV+RX~0-`AI^m=!KOcE^FX~2wka!NuNjklHMVz0jbPUs_F zhUC+0)CyNUb27uVEOBis&8^=%a{I_i)uuW7tbN{-8p&4e!jg5?ddrR($E;(1?=6?! zV_Rxsq5DoWTXkSfFC4t>9({Y~z1`WW=e2RSxK)>}r83#`t$|gTnF?s=i+RHOg#B@4 zb5w$wz>{D1Bn(yqNu>upv{Vm0JSM3DUMhW6Mv6QyHDS@J32D5w{84DlyP&U(M|O?M z)zr_uIQwFXzSWiHo{^fN(qnq!hS!s0m}jI`czwnJk_N`T!(v2^N~%Q?0bwK3X6#`@ z4vF}~B5;Vq=L<)LSP1i+&-ay>Kcux-eLf)?fNFCvqL9xewZUlBsWC!84266?*j*#k z%6JaP$d)AJRFdFiGLHH5I`vCi|5L_cuS^B<6ucHs{2pJQG|=`AvIR0=4NmHiv5HD- z9ZH%;03!hxnd{VuMPNm%XcO(CW7LFe!GFP0S~JRk&SgL&+e&LU^sz%9PH_CeJ{8c% zxq*&VwBxKW4+rg)W!ha@yA#^0gvyO89yJLrXsg~Z)&*3npk>okwP%xBy#e;)fA0Wc zwcxXf1vyLKrfLO)!sjN?&V=$PyZR!@ltx}onw?qr^&xE3PZs=LTEzJO-8$kH<~!y0(SFGs%l zXYe&kZ41eipyejT_{I~4WdS3!r>Nnn}3Fet*rdbn}FijZWy`(1t^$5o6o_DP}=S*8ZcP#tI#!l3%BMU{rrt{ad6~-gj zuX`p?$EL1)?7=+IvaTx>_1@EVp<>#hp!||2;m}vv zLw$J^&amQhRdV4QpjFK*vBQgABI>$OF`(zyfplPlMra0r!eo55&s#g7al z;1V~+u$mBj(E}@ZI7NvvAfK1SKvWWBZ~O%^JQx|{hU{$)v5`6~i#5ZsF2HtgD&H*X;1<+#G zsNcHUGtV7HEFYX1OgF!>#C-kILjo2ltbbA7+DxqoeIecar>+`K!}ygR+8FWY=F zV{1s-Za$mi8j`(l4a^M8H{7fJQS1HICGKF7F4oqiVt1$SOfPZ!m$|MC*Y%Lja>tYO zV|U}+>DklE?#_(6Gc|no^*gUGZ#kUVayYx?$ivRGyEE%PnY8EJZ8wk4otiy$YanTV zTwRm2tW?+CynL(mBg$fLO?KzFD%3v9Tx*7diQ98r?K0P%;o6tD&AHmfxz}f3PgUP~ zBiZ-MnwI%jQs=WZ&n0_s(3zPtH_k$D-0z26OWq0O@>NtL(RnMj%C{1sl-O-yq|HX{!)wJ!^f~w<4V8cfO8*+K=wv}i%z(&J@fwR}1 zB3ewFiWnpP0D7ZEwC<6<1NDZzQ*8X&pMwUZL(DYzf9XpUr5O5h0zI~Mk3OHkzRgTqfN~jqIbmqX;^3mb0*z-G zqB^MEV4__yVaSe#zZ=K3pq(Y&t(*23m;;>FB56|?=kgc9e=K+z!efD$Bw=8!B)lej z2MJh9FTnI;g(W=h0vErcgqUhq#=%h^kA?)*6$)MwLsN#O25rGGc=LeAo+`CQ<89Ck z2D%oInZO>xCqX%t5idi?!3sJAK~g$_4Lz9jLZY$+hrvXJ7@b<@GkgUAEn|l&gA+?$ z>~A3+jNq{rz;p!bDm4i86#|C^a7FJ|g6{@HU_-HxA|n%N2w8x~77ZAU9hOd7MqhdV zQ2(z0b@|^Rxklw}R8!k>-*V14mu)Q>8(`#;tuyDU zO*%hm)ro#4qv`ypy}Pc*X8x&-?Wwk^Hr=FO19xbmo8f=;1|;8LAzDJ013ZBqGeT`( zri0-gqQfR)rkl$_*Ehj{mO(VpphpA0#KkiuS%{cguiM_W>Eb@kBpAV-V7^QJwMlRY z6~w&a;=K^gCdQR!heUZf1|AbwRw!c^F+-wOF^mNBTMg#qgQI9Mkx?Em@FWJYWg=Z* z^0>OO*ezWQf^G$aWKeYi^A`z$73I7VjG*1(BO`%80B6|X_@WE2x)l-(vCfn|w*-bX zHHQdHMpHq?VHM^vQGuCgKNvA!I)DWMG512u=^A1G&ulM-{Nam&|7iU1MzcU?bv%TU zwR>L-MZx8lk3yy}_%C32@+L?=y+++T^rNHqk1lRrJfGd(b&VnBn*^LbW{g&515{jfaqZ*8LMi@nZfmg*KZuA6v38FJ-vP1FAog+&Rys~ zd!c{0zjsJAhy7PQmI5qXLb72@NOub!w$wq<_&o+(?sJ!$#1?O?99bGi0lruJac@k>`NxY7&0Oigc^>s_gK-+U#z zxofHVSkm&iwIkQCIrVI+AzimE*VOjZ%2YY>6k~Ugb6g`-C{RSG-iFK{4?s?v#wf4> zz!pG-C+!8@jHA~*rO}Je>0#FODQK(mGz_hbJ%AzE_|73srV30>?qA@@K}#7oP9>L6 zvR8To)}?}m6MYaU0&Y`81Za#SivnTAm=pkipd%7hf}>Lf5t4n;QQu?~jC+iv7=W&} z1|tDK$Hc<0EBK-BBAiW6a-@@U@{3g5(1{^B}+u3fXL{#IjU7#w;t~<1W(i^ z4Jy#O#=U^r^=UoJgnYc8-}^SqDi1&ejz~T?+GD38uXV{4jCuwRQp@SzL?6|Ynj)V1UR^9e#Q`hBL(J^~xB0F%lH->m6x`9Joz zLRdsw&HswNeIxy`61KhZ_42!evvRy+7>8h*-DV&#BrszTgq$$0W%PZ+tp@Ba4n@IK zzP$;~KLMJ{QxQJ^NCPmp)?CATN5NYRk$3`E1>SOlW9s9#hr5QV@1T>W8b5d6x?m@g)S4*H$GYz>=VJEn4BF&veq z3hinIpeFz6NYXAS z>41$=C!NFQN?4%?(x=I_>L4nT+*82C7TyyV5nPL7z6s;vTQ;I~84LA3bd~=R5&&xc znWRl^^d&2EOk*xdZ!`h6yPp@rVq|5YeU+l;>9nHGC@^*Ifrr%R+bxppK z0>uHde8BJfuxfw47S@w*plVxF=J{Akx%<^SU(K}d&$#!e51w3f|3lkP+R`;IC2cuZ zebQC%IIUXPXN{u3z&Faq>YF7p5AsffyuX9;t{Bc|f+;7{`0EATzR;y>nk|kh%JAom zHy7S(>s1eqO9coVk&;aDE%=jis zpO+}Fa%0P%fW7$J3&|u@MCj6AR^0eLqCmntijvI5yLt-&Cw{B$lZ5?>6ui2FC&3-f zL-FwBw+|(;J3VBl+GNlsV607%NE)$WG?{cC(=rXm3Sx$MKB!iMtdpN__@V*g^$EjT zzkCdvWqR~I*BJ73K5M?iRzr*k#U^MTo;sly93nT~zwP5T#X7TJgH zpS3+|OJ5kubPr{|Fr3*moIH`^+;5$oIXizM&F@Ne-u2$`rdxI|aeHzQ*@BQys_t&n zou>4Tu7~cW&3(5!-aqjncMfAe=V=his)qTxw5vUBYu~6)k;#Z~{S##1Zi)s#FKeVf zfOdR9=)mcnHY+%aJP9+pI=cFTNJ9^N32<#d9N_ap?5DJ+B}nN#%iergi=qaK+*G1e z4fQ72hyW4VH%>BZ+$O=U3p%i}BYsr-4N1}KF)r@#7bH5{tqDbdjTQuk2kqi5@aS8x zOQJ8HpstrNnxxtwpz6oaG8*_0dR=SRjTL^Odc zHAyyCyLq{GYo>NXWs15BDyP|D-Lm@1;D2mG2LyFT9-o;z;_!$j{$cB~|(3bOT$LYvEHhsX(gX_F&!& z<;QksGH`Qma(t$3m4$MCAICZtn1!Lmj)hm>tIShSe7Ns@^ZP^Z+a4WV#bUn2!Pc*# z17$*Irg4>pe4ejn8%k+F5ooZF4zE&>%|Gj8`CNVTe2>18O#Sv%GgRi=Y%IU9Z_&Is zv}k+pXr6-NL-AvL5ey*b;&GGnSFL!*3QmPI3I_$Z3saL=Am@&sq3}TvD8K<0l>u@g zTlkQ8fgv5kq#KiJOz>tyt>_O=MkS>eEiS1GYbQJT2e+o(7Ow;sJlZ9qht0>y_E2`pOs2y2q2Lyl|&B^*@dsD{VH2;OHz4MwS zZ?V!}rhmm${hDcfT;H5#+)v#URVt;j&Xcr@N0wVn%DoC`BI+6hR Dict[str, Any]: + return { + "strategy": self.strategy.value, + "winner": self.winner, + "allocation": self.allocation, + "rationale": self.rationale, + "timestamp": self.timestamp, + } + + +class AgentArbitrator: + """ + Arbitrate between competing agent requests. + + Default agent priorities (higher number = higher priority): + resolver → 6 (highest; can override all) + perception → 5 + planning → 4 + control → 3 + communication → 2 + coordination → 1 + """ + + DEFAULT_PRIORITIES: Dict[str, int] = { + "perception": 5, + "planning": 4, + "control": 3, + "communication": 2, + "coordination": 1, + "resolver": 6, + } + + def __init__(self, agent_priorities: Optional[Dict[str, int]] = None): + self.agent_priorities = agent_priorities or dict(self.DEFAULT_PRIORITIES) + self._round_robin_index: Dict[str, int] = {} + + # ------------------------------------------------------------------ # + # Public API + # ------------------------------------------------------------------ # + + def arbitrate_control_request( + self, + requests: List[Dict[str, Any]], + strategy: ArbitrationStrategy = ArbitrationStrategy.PRIORITY_QUEUE, + ) -> Dict[str, Any]: + """Determine which agent gets control of the robot.""" + if not requests: + return ArbitrationResult( + strategy=strategy, + winner=None, + allocation={}, + rationale="No requests to arbitrate", + ).to_dict() + + # Emergency requests always override normal priority + emergency = [r for r in requests if r.get("emergency", False)] + if emergency: + return self._emergency_override(emergency, "control") + + return self._priority_queue_arbitration(requests, "control") + + def arbitrate_resource_allocation( + self, requests: List[Dict[str, Any]] + ) -> Dict[str, Any]: + """Allocate limited resources (CPU, memory, sensors) among requesting agents.""" + if not requests: + return ArbitrationResult( + strategy=ArbitrationStrategy.PRIORITY_QUEUE, + winner=None, + allocation={}, + rationale="No resource requests", + ).to_dict() + + # Group requests by resource + resource_groups: Dict[str, List[Dict[str, Any]]] = {} + for req in requests: + resource = req.get("resource", "unknown") + resource_groups.setdefault(resource, []).append(req) + + allocation: Dict[str, str] = {} + for resource, group in resource_groups.items(): + winner = max(group, key=lambda r: self.agent_priorities.get(r.get("agent", ""), 0)) + allocation[resource] = winner.get("agent", "") + + return ArbitrationResult( + strategy=ArbitrationStrategy.PRIORITY_QUEUE, + winner=None, + allocation=allocation, + rationale="Resources allocated by agent priority", + ).to_dict() + + def arbitrate_task_priority( + self, tasks: List[Dict[str, Any]] + ) -> Dict[str, Any]: + """Determine task execution order.""" + if not tasks: + return ArbitrationResult( + strategy=ArbitrationStrategy.PRIORITY_QUEUE, + winner=None, + allocation={}, + rationale="No tasks to arbitrate", + ).to_dict() + + sorted_tasks = sorted( + tasks, + key=lambda t: ( + t.get("priority", 0), + self.agent_priorities.get(t.get("agent", ""), 0), + ), + reverse=True, + ) + + return ArbitrationResult( + strategy=ArbitrationStrategy.PRIORITY_QUEUE, + winner=sorted_tasks[0].get("agent"), + allocation={"ordered_tasks": [t.get("id", str(i)) + for i, t in enumerate(sorted_tasks)]}, + rationale="Tasks sorted by priority then agent priority", + ).to_dict() + + def arbitrate_with_context( + self, requests: List[Dict[str, Any]], context: Dict[str, Any] + ) -> Dict[str, Any]: + """Context-aware arbitration (e.g., emergency overrides normal priority).""" + if context.get("emergency"): + return self._emergency_override(requests, "context_aware") + return self.arbitrate_control_request(requests) + + # ------------------------------------------------------------------ # + # Internal helpers + # ------------------------------------------------------------------ # + + def _priority_queue_arbitration( + self, requests: List[Dict[str, Any]], resource_type: str + ) -> Dict[str, Any]: + winner_req = max( + requests, + key=lambda r: ( + r.get("priority", 0), + self.agent_priorities.get(r.get("agent", ""), 0), + ), + ) + winner = winner_req.get("agent", "") + return ArbitrationResult( + strategy=ArbitrationStrategy.PRIORITY_QUEUE, + winner=winner, + allocation={resource_type: winner}, + rationale=f"Agent '{winner}' selected by priority queue for {resource_type}", + ).to_dict() + + def _emergency_override( + self, requests: List[Dict[str, Any]], resource_type: str + ) -> Dict[str, Any]: + """Grant control to the highest-priority emergency request.""" + winner_req = max( + requests, + key=lambda r: self.agent_priorities.get(r.get("agent", ""), 0), + ) + winner = winner_req.get("agent", "") + return ArbitrationResult( + strategy=ArbitrationStrategy.EMERGENCY_OVERRIDE, + winner=winner, + allocation={resource_type: winner}, + rationale=f"Emergency override: '{winner}' granted {resource_type}", + ).to_dict() + + def _round_robin( + self, requests: List[Dict[str, Any]], resource_type: str + ) -> Dict[str, Any]: + idx = self._round_robin_index.get(resource_type, 0) % len(requests) + winner = requests[idx].get("agent", "") + self._round_robin_index[resource_type] = idx + 1 + return ArbitrationResult( + strategy=ArbitrationStrategy.ROUND_ROBIN, + winner=winner, + allocation={resource_type: winner}, + rationale=f"Round-robin selection: index {idx}", + ).to_dict() diff --git a/resolver/conflict_resolution.py b/resolver/conflict_resolution.py new file mode 100644 index 0000000..ab678a9 --- /dev/null +++ b/resolver/conflict_resolution.py @@ -0,0 +1,379 @@ +""" +Conflict resolution module for the Resolver Agent. + +Provides: +- ConflictDetector – detect conflicts between agents +- ConflictResolver – apply resolution strategies +- ConflictPredictor – rule-based conflict prediction (ML version in predictor.py) +""" + +import logging +import time +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Dict, List, Optional + + +logger = logging.getLogger(__name__) + + +class ConflictType(Enum): + """Enumeration of recognised conflict categories.""" + PERCEPTION_PLANNING = "perception_planning" + CONTROL = "control" + TASK = "task" + RESOURCE = "resource" + COMMUNICATION = "communication" + UNKNOWN = "unknown" + + +class ResolutionStrategy(Enum): + """Enumeration of available resolution strategies.""" + PRIORITY_BASED = "priority_based" + VOTING = "voting" + EXPERTISE = "expertise" + TIME_BASED = "time_based" + COST_BASED = "cost_based" + ML_BASED = "ml_based" + + +@dataclass +class Conflict: + """Data class describing a detected conflict.""" + conflict_type: ConflictType + agents: List[str] + description: str + details: Dict[str, Any] = field(default_factory=dict) + severity: int = 1 + timestamp: float = field(default_factory=time.time) + + def to_dict(self) -> Dict[str, Any]: + return { + "type": self.conflict_type.value, + "agents": self.agents, + "description": self.description, + "details": self.details, + "severity": self.severity, + "timestamp": self.timestamp, + } + + +@dataclass +class Resolution: + """Data class describing the outcome of a conflict resolution.""" + strategy: ResolutionStrategy + winning_agent: Optional[str] + action: str + rationale: str + success: bool = True + timestamp: float = field(default_factory=time.time) + + def to_dict(self) -> Dict[str, Any]: + return { + "strategy": self.strategy.value, + "winning_agent": self.winning_agent, + "action": self.action, + "rationale": self.rationale, + "success": self.success, + "timestamp": self.timestamp, + } + + +# --------------------------------------------------------------------------- +# ConflictDetector +# --------------------------------------------------------------------------- + +class ConflictDetector: + """ + Detect conflicts between agents. + + Supports detection of: + - Perception vs Planning disagreements + - Competing control commands + - Overlapping or contradictory tasks + - Resource contention + """ + + def detect_all(self, agent_states: Dict[str, Any]) -> List[Dict[str, Any]]: + """Run all detectors and return a list of detected conflicts.""" + conflicts: List[Dict[str, Any]] = [] + conflicts.extend(self.detect_perception_planning_conflict(agent_states)) + conflicts.extend(self.detect_control_conflict(agent_states)) + conflicts.extend(self.detect_task_conflict(agent_states)) + conflicts.extend(self.detect_resource_conflict(agent_states)) + return conflicts + + def detect_perception_planning_conflict( + self, agent_states: Dict[str, Any] + ) -> List[Dict[str, Any]]: + """Detect conflicts between Perception and Planning agents.""" + conflicts: List[Dict[str, Any]] = [] + perception = agent_states.get("perception") + planning = agent_states.get("planning") + + if perception and planning: + perception_meta = getattr(perception, "metadata", {}) + planning_meta = getattr(planning, "metadata", {}) + p_objects = set(perception_meta.get("detected_objects", [])) + pl_objects = set(planning_meta.get("known_objects", [])) + disagreements = p_objects.symmetric_difference(pl_objects) + + if disagreements: + conflict = Conflict( + conflict_type=ConflictType.PERCEPTION_PLANNING, + agents=["perception", "planning"], + description="Object detection disagreement between Perception and Planning", + details={ + "perception_objects": list(p_objects), + "planning_objects": list(pl_objects), + "disagreements": list(disagreements), + }, + severity=2, + ) + conflicts.append(conflict.to_dict()) + return conflicts + + def detect_control_conflict(self, agent_states: Dict[str, Any]) -> List[Dict[str, Any]]: + """Detect conflicts where multiple agents compete for robot control.""" + conflicts: List[Dict[str, Any]] = [] + control_requesting = [ + name for name, state in agent_states.items() + if getattr(state, "metadata", {}).get("requesting_control", False) + ] + + if len(control_requesting) > 1: + conflict = Conflict( + conflict_type=ConflictType.CONTROL, + agents=control_requesting, + description="Multiple agents requesting simultaneous robot control", + details={"requesters": control_requesting}, + severity=3, + ) + conflicts.append(conflict.to_dict()) + return conflicts + + def detect_task_conflict(self, agent_states: Dict[str, Any]) -> List[Dict[str, Any]]: + """Detect overlapping or contradictory task assignments.""" + conflicts: List[Dict[str, Any]] = [] + task_assignments: Dict[str, List[str]] = {} + + for agent_name, state in agent_states.items(): + tasks = getattr(state, "metadata", {}).get("assigned_tasks", []) + for task_id in tasks: + task_assignments.setdefault(task_id, []).append(agent_name) + + for task_id, agents in task_assignments.items(): + if len(agents) > 1: + conflict = Conflict( + conflict_type=ConflictType.TASK, + agents=agents, + description=f"Task '{task_id}' assigned to multiple agents", + details={"task_id": task_id, "assigned_agents": agents}, + severity=2, + ) + conflicts.append(conflict.to_dict()) + return conflicts + + def detect_resource_conflict(self, agent_states: Dict[str, Any]) -> List[Dict[str, Any]]: + """Detect when multiple agents compete for the same resource.""" + conflicts: List[Dict[str, Any]] = [] + resource_requests: Dict[str, List[str]] = {} + + for agent_name, state in agent_states.items(): + resources = getattr(state, "metadata", {}).get("requested_resources", []) + for resource in resources: + resource_requests.setdefault(resource, []).append(agent_name) + + for resource, agents in resource_requests.items(): + if len(agents) > 1: + conflict = Conflict( + conflict_type=ConflictType.RESOURCE, + agents=agents, + description=f"Resource '{resource}' requested by multiple agents", + details={"resource": resource, "requesting_agents": agents}, + severity=2, + ) + conflicts.append(conflict.to_dict()) + return conflicts + + +# --------------------------------------------------------------------------- +# ConflictResolver +# --------------------------------------------------------------------------- + +# Default agent priorities used for priority-based resolution +_DEFAULT_PRIORITIES: Dict[str, int] = { + "perception": 5, + "planning": 4, + "control": 3, + "communication": 2, + "coordination": 1, + "resolver": 6, +} + + +class ConflictResolver: + """ + Apply resolution strategies to detected conflicts. + + Available strategies: + 1. Priority-based (higher priority wins) + 2. Voting (majority rules) + 3. Expert arbitration (most qualified agent decides) + 4. Time-based (first-come-first-served) + 5. Cost-based (least cost approach) + 6. ML-based (learned from past resolutions) + """ + + def __init__(self, agent_priorities: Optional[Dict[str, int]] = None): + self._priorities = agent_priorities or _DEFAULT_PRIORITIES + self._resolution_history: List[Dict[str, Any]] = [] + + def resolve(self, conflict: Dict[str, Any], + strategy: ResolutionStrategy = ResolutionStrategy.PRIORITY_BASED + ) -> Dict[str, Any]: + """Resolve a conflict using the specified strategy.""" + strategy_map = { + ResolutionStrategy.PRIORITY_BASED: self.resolve_by_priority, + ResolutionStrategy.VOTING: self.resolve_by_voting, + ResolutionStrategy.EXPERTISE: self.resolve_by_expertise, + ResolutionStrategy.COST_BASED: self.resolve_by_cost, + ResolutionStrategy.ML_BASED: self.resolve_by_ml, + } + handler = strategy_map.get(strategy, self.resolve_by_priority) + resolution = handler(conflict) + self._resolution_history.append(resolution.to_dict()) + logger.info("Conflict resolved via %s: %s", strategy.value, resolution.action) + return resolution.to_dict() + + def resolve_by_priority(self, conflict: Dict[str, Any]) -> Resolution: + """Resolve by selecting the highest-priority agent.""" + agents = conflict.get("agents", []) + if not agents: + return Resolution( + strategy=ResolutionStrategy.PRIORITY_BASED, + winning_agent=None, + action="no_agents_involved", + rationale="No agents listed in conflict", + success=False, + ) + winner = max(agents, key=lambda a: self._priorities.get(a, 0)) + return Resolution( + strategy=ResolutionStrategy.PRIORITY_BASED, + winning_agent=winner, + action=f"grant_control_to_{winner}", + rationale=f"Agent '{winner}' has highest priority " + f"({self._priorities.get(winner, 0)})", + ) + + def resolve_by_voting(self, conflict: Dict[str, Any]) -> Resolution: + """Resolve by majority vote among connected agents (simplified simulation).""" + agents = conflict.get("agents", []) + # In a real system this would broadcast a vote request; here we use priorities + votes: Dict[str, int] = {a: self._priorities.get(a, 0) for a in agents} + if not votes: + return Resolution( + strategy=ResolutionStrategy.VOTING, + winning_agent=None, + action="no_votes", + rationale="No agents to vote", + success=False, + ) + winner = max(votes, key=lambda a: votes[a]) + return Resolution( + strategy=ResolutionStrategy.VOTING, + winning_agent=winner, + action=f"grant_control_to_{winner}", + rationale=f"Agent '{winner}' received highest weighted vote ({votes[winner]})", + ) + + def resolve_by_expertise(self, conflict: Dict[str, Any]) -> Resolution: + """Resolve by selecting the most domain-relevant agent.""" + conflict_type = conflict.get("type", ConflictType.UNKNOWN.value) + expertise_map: Dict[str, str] = { + ConflictType.PERCEPTION_PLANNING.value: "perception", + ConflictType.CONTROL.value: "control", + ConflictType.TASK.value: "coordination", + ConflictType.RESOURCE.value: "resolver", + } + expert = expertise_map.get(conflict_type, "resolver") + return Resolution( + strategy=ResolutionStrategy.EXPERTISE, + winning_agent=expert, + action=f"delegate_decision_to_{expert}", + rationale=f"Agent '{expert}' has domain expertise for conflict type '{conflict_type}'", + ) + + def resolve_by_cost(self, conflict: Dict[str, Any]) -> Resolution: + """Resolve by selecting the lowest-cost option from conflict details.""" + options = conflict.get("details", {}).get("options", []) + if not options: + return self.resolve_by_priority(conflict) + + best = min(options, key=lambda o: o.get("cost", float("inf"))) + return Resolution( + strategy=ResolutionStrategy.COST_BASED, + winning_agent=best.get("agent"), + action=f"select_option_{best.get('id', 'best')}", + rationale=f"Option has minimum cost: {best.get('cost')}", + ) + + def resolve_by_ml(self, conflict: Dict[str, Any]) -> Resolution: + """Resolve using a trained ML model (falls back to priority-based if unavailable).""" + try: + from resolver.predictor import ConflictPredictor # pylint: disable=import-outside-toplevel + predictor = ConflictPredictor() + prediction = predictor.predict_best_resolution(conflict) + return Resolution( + strategy=ResolutionStrategy.ML_BASED, + winning_agent=prediction.get("winning_agent"), + action=prediction.get("action", "ml_resolution"), + rationale="ML model prediction", + ) + except Exception: # pylint: disable=broad-except + logger.warning("ML resolution unavailable, falling back to priority-based") + return self.resolve_by_priority(conflict) + + @property + def resolution_history(self) -> List[Dict[str, Any]]: + """Return the full history of resolutions applied.""" + return list(self._resolution_history) + + +# --------------------------------------------------------------------------- +# ConflictPredictor (rule-based; ML version is in predictor.py) +# --------------------------------------------------------------------------- + +class ConflictPredictor: + """ + Rule-based conflict predictor. + + Analyses current agent states and identifies patterns that historically + precede conflicts so that preventive actions can be taken. + """ + + def predict(self, agent_states: Dict[str, Any]) -> List[Dict[str, Any]]: + """Return a list of predicted conflicts with prevention suggestions.""" + predictions: List[Dict[str, Any]] = [] + + # Rule: If multiple agents are requesting the same resource, a conflict is imminent + resource_counts: Dict[str, int] = {} + for state in agent_states.values(): + for resource in getattr(state, "metadata", {}).get("requested_resources", []): + resource_counts[resource] = resource_counts.get(resource, 0) + 1 + + for resource, count in resource_counts.items(): + if count > 1: + predictions.append({ + "type": ConflictType.RESOURCE.value, + "probability": min(0.5 + count * 0.1, 0.99), + "description": f"Resource '{resource}' contention likely", + "prevention": f"Pre-allocate '{resource}' before conflict occurs", + }) + + return predictions + + def suggest_preventive_actions(self, agent_states: Dict[str, Any]) -> List[str]: + """Return a list of preventive action strings.""" + predictions = self.predict(agent_states) + return [p["prevention"] for p in predictions] diff --git a/resolver/deadlock_detector.py b/resolver/deadlock_detector.py new file mode 100644 index 0000000..5d53cdb --- /dev/null +++ b/resolver/deadlock_detector.py @@ -0,0 +1,180 @@ +""" +Deadlock detection and breaking for the Resolver Agent. + +Detects circular waits, resource deadlocks, and task deadlocks, +then applies breaking strategies to restore progress. +""" + +import logging +import time +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Set + + +logger = logging.getLogger(__name__) + + +@dataclass +class Deadlock: + """Represents a detected deadlock.""" + deadlock_type: str + involved_agents: List[str] + description: str + details: Dict[str, Any] = field(default_factory=dict) + detected_at: float = field(default_factory=time.time) + + def to_dict(self) -> Dict[str, Any]: + return { + "deadlock_type": self.deadlock_type, + "involved_agents": self.involved_agents, + "description": self.description, + "details": self.details, + "detected_at": self.detected_at, + } + + +class DeadlockDetector: + """ + Detect and break deadlocks in the multi-agent system. + + Deadlock types handled: + - Circular dependencies (A waits for B, B waits for A) + - Resource deadlocks (all agents waiting for held resources) + - Task deadlocks (tasks cannot proceed) + """ + + TIMEOUT_THRESHOLD = 30.0 # seconds a wait may last before suspecting deadlock + + def __init__(self): + # wait_graph[agent] = set of agents this agent is waiting for + self._wait_graph: Dict[str, Set[str]] = {} + # resource_holders[resource] = agent currently holding it + self._resource_holders: Dict[str, str] = {} + # resource_waiters[resource] = list of agents waiting for it + self._resource_waiters: Dict[str, List[str]] = {} + self._detected_deadlocks: List[Deadlock] = [] + + # ------------------------------------------------------------------ # + # Public API + # ------------------------------------------------------------------ # + + def update_wait_graph(self, agent: str, waiting_for: List[str]) -> None: + """Record that *agent* is waiting for the listed agents.""" + self._wait_graph[agent] = set(waiting_for) + + def update_resource_state( + self, resource: str, holder: Optional[str], waiters: Optional[List[str]] = None + ) -> None: + """Update resource holder and waiter information.""" + if holder: + self._resource_holders[resource] = holder + elif resource in self._resource_holders: + del self._resource_holders[resource] + self._resource_waiters[resource] = waiters or [] + + def detect(self) -> Optional[Dict[str, Any]]: + """Run all deadlock detection heuristics and return the first found.""" + deadlock = self.detect_circular_wait() + if deadlock: + return deadlock + return self.detect_resource_deadlock() + + def detect_circular_wait(self) -> Optional[Dict[str, Any]]: + """Detect circular waiting chains in the wait graph (DFS cycle detection).""" + cycle = self._find_cycle() + if cycle: + deadlock = Deadlock( + deadlock_type="circular_wait", + involved_agents=cycle, + description=f"Circular wait detected: {' -> '.join(cycle)}", + details={"cycle": cycle}, + ) + self._detected_deadlocks.append(deadlock) + logger.warning("Circular wait deadlock detected: %s", cycle) + return deadlock.to_dict() + return None + + def detect_resource_deadlock(self) -> Optional[Dict[str, Any]]: + """Detect resource deadlocks where every waiter is also a holder.""" + for resource, waiters in self._resource_waiters.items(): + holder = self._resource_holders.get(resource) + if holder and holder in waiters: + deadlock = Deadlock( + deadlock_type="resource_deadlock", + involved_agents=[holder] + waiters, + description=f"Resource deadlock on '{resource}': " + f"holder '{holder}' is also waiting", + details={"resource": resource, "holder": holder, "waiters": waiters}, + ) + self._detected_deadlocks.append(deadlock) + logger.warning("Resource deadlock detected on '%s'", resource) + return deadlock.to_dict() + return None + + def break_deadlock(self, deadlock: Dict[str, Any]) -> Dict[str, Any]: + """Break a deadlock using the most appropriate strategy.""" + deadlock_type = deadlock.get("deadlock_type", "unknown") + agents = deadlock.get("involved_agents", []) + + if not agents: + return {"action": "no_agents", "success": False} + + # Strategy: remove the lowest-priority agent from the cycle + from resolver.arbitrator import AgentArbitrator # pylint: disable=import-outside-toplevel + priorities = AgentArbitrator.DEFAULT_PRIORITIES + victim = min(agents, key=lambda a: priorities.get(a, 0)) + + action = { + "action": "preempt_agent", + "victim": victim, + "deadlock_type": deadlock_type, + "rationale": f"Agent '{victim}' preempted to break deadlock", + "success": True, + } + + # Remove the victim from the wait graph + self._wait_graph.pop(victim, None) + for agent_waiters in self._wait_graph.values(): + agent_waiters.discard(victim) + + logger.info("Deadlock broken: preempted agent '%s'", victim) + return action + + @property + def detected_deadlocks(self) -> List[Dict[str, Any]]: + """Return history of all detected deadlocks.""" + return [d.to_dict() for d in self._detected_deadlocks] + + # ------------------------------------------------------------------ # + # Internal helpers + # ------------------------------------------------------------------ # + + def _find_cycle(self) -> Optional[List[str]]: + """Return the nodes of a cycle in the wait graph, or None if none exists.""" + visited: Set[str] = set() + path: List[str] = [] + path_set: Set[str] = set() + + def dfs(node: str) -> Optional[List[str]]: + visited.add(node) + path.append(node) + path_set.add(node) + for neighbour in self._wait_graph.get(node, set()): + if neighbour not in visited: + result = dfs(neighbour) + if result is not None: + return result + elif neighbour in path_set: + # Found a cycle; extract the cycle portion + cycle_start = path.index(neighbour) + return path[cycle_start:] + path.pop() + path_set.discard(node) + return None + + for node in list(self._wait_graph.keys()): + if node not in visited: + cycle = dfs(node) + if cycle: + return cycle + return None diff --git a/resolver/error_recovery.py b/resolver/error_recovery.py new file mode 100644 index 0000000..f091fce --- /dev/null +++ b/resolver/error_recovery.py @@ -0,0 +1,208 @@ +""" +Error recovery system for the Resolver Agent. + +Classifies errors by severity and type, then executes the appropriate +recovery strategy automatically. +""" + +import logging +import time +from dataclasses import dataclass, field +from enum import IntEnum +from typing import Any, Callable, Dict, List, Optional + + +logger = logging.getLogger(__name__) + + +class ErrorSeverity(IntEnum): + """Error severity levels, ordered from least to most severe.""" + INFO = 0 # Minor issue; log only + WARNING = 1 # Potential problem; monitor + ERROR = 2 # Significant issue; needs recovery + CRITICAL = 3 # System-threatening; immediate action required + FATAL = 4 # System shutdown required + + +class ErrorType: + """Constants for recognised error types.""" + SENSOR_FAILURE = "sensor_failure" + AGENT_CRASH = "agent_crash" + PLANNING_FAILURE = "planning_failure" + EXECUTION_FAILURE = "execution_failure" + COMMUNICATION_FAILURE = "communication_failure" + HARDWARE_FAILURE = "hardware_failure" + UNKNOWN = "unknown" + + +@dataclass +class ErrorRecord: + """Record of a detected error and its recovery attempt.""" + error_type: str + severity: ErrorSeverity + description: str + details: Dict[str, Any] = field(default_factory=dict) + timestamp: float = field(default_factory=time.time) + recovery_attempted: bool = False + recovery_success: Optional[bool] = None + recovery_action: str = "" + + def to_dict(self) -> Dict[str, Any]: + return { + "error_type": self.error_type, + "severity": self.severity.name, + "description": self.description, + "details": self.details, + "timestamp": self.timestamp, + "recovery_attempted": self.recovery_attempted, + "recovery_success": self.recovery_success, + "recovery_action": self.recovery_action, + } + + +class ErrorRecoverySystem: + """ + Detect, classify, and recover from errors across all robot systems. + + Supported error types: + - Sensor failures (camera down, lidar malfunction) + - Agent crashes (agent stops responding) + - Planning failures (no valid plan found) + - Execution failures (robot cannot reach goal) + - Communication failures (lost connection) + - Hardware failures (motor error, battery low) + """ + + MAX_RECOVERY_ATTEMPTS = 3 + + def __init__(self): + self._error_log: List[ErrorRecord] = [] + self._recovery_attempts: Dict[str, int] = {} + self._recovery_handlers: Dict[str, Callable[[Dict[str, Any]], str]] = { + ErrorType.SENSOR_FAILURE: self._recover_sensor_failure, + ErrorType.AGENT_CRASH: self._recover_agent_crash, + ErrorType.PLANNING_FAILURE: self._recover_planning_failure, + ErrorType.EXECUTION_FAILURE: self._recover_execution_failure, + ErrorType.COMMUNICATION_FAILURE: self._recover_communication_failure, + ErrorType.HARDWARE_FAILURE: self._recover_hardware_failure, + } + + # ------------------------------------------------------------------ # + # Public API + # ------------------------------------------------------------------ # + + def detect_errors(self, agent_states: Dict[str, Any]) -> List[Dict[str, Any]]: + """Scan agent states and return a list of detected error dicts.""" + errors: List[Dict[str, Any]] = [] + now = time.time() + + for agent_name, state in agent_states.items(): + heartbeat = getattr(state, "last_heartbeat", now) + if now - heartbeat > 30: # No heartbeat for 30 s → agent crash + errors.append({ + "type": ErrorType.AGENT_CRASH, + "severity": ErrorSeverity.CRITICAL, + "agent": agent_name, + "description": f"Agent '{agent_name}' has not sent a heartbeat in 30s", + }) + + error_count = getattr(state, "error_count", 0) + if error_count > 10: + errors.append({ + "type": ErrorType.UNKNOWN, + "severity": ErrorSeverity.WARNING, + "agent": agent_name, + "description": f"Agent '{agent_name}' has high error count: {error_count}", + }) + return errors + + def classify_error(self, error: Dict[str, Any]) -> ErrorRecord: + """Classify an error dict and return a structured ErrorRecord.""" + error_type = error.get("type", ErrorType.UNKNOWN) + raw_severity = error.get("severity", ErrorSeverity.ERROR) + if isinstance(raw_severity, int): + severity = ErrorSeverity(raw_severity) + elif isinstance(raw_severity, ErrorSeverity): + severity = raw_severity + else: + severity = ErrorSeverity.ERROR + + return ErrorRecord( + error_type=error_type, + severity=severity, + description=error.get("description", "No description provided"), + details={k: v for k, v in error.items() + if k not in {"type", "severity", "description"}}, + ) + + def execute_recovery(self, error: Dict[str, Any]) -> Dict[str, Any]: + """Execute the appropriate recovery strategy for an error.""" + record = self.classify_error(error) + error_key = f"{record.error_type}:{record.details.get('agent', 'system')}" + + attempts = self._recovery_attempts.get(error_key, 0) + if attempts >= self.MAX_RECOVERY_ATTEMPTS: + record.recovery_attempted = True + record.recovery_success = False + record.recovery_action = "max_recovery_attempts_exceeded" + self._error_log.append(record) + logger.error("Max recovery attempts exceeded for: %s", error_key) + return {**record.to_dict(), "escalation_required": True} + + self._recovery_attempts[error_key] = attempts + 1 + handler = self._recovery_handlers.get(record.error_type, self._recover_unknown) + recovery_action = handler(error) + + record.recovery_attempted = True + record.recovery_success = True + record.recovery_action = recovery_action + self._error_log.append(record) + + logger.info("Recovery executed for %s: %s", error_key, recovery_action) + return record.to_dict() + + @property + def error_log(self) -> List[Dict[str, Any]]: + """Return the full error log as a list of dicts.""" + return [r.to_dict() for r in self._error_log] + + # ------------------------------------------------------------------ # + # Recovery strategies + # ------------------------------------------------------------------ # + + def _recover_sensor_failure(self, error: Dict[str, Any]) -> str: + """Switch to backup sensors or degrade gracefully.""" + sensor = error.get("sensor", "unknown") + logger.warning("Sensor failure detected: %s. Switching to backup sensor.", sensor) + return f"switched_to_backup_sensor:{sensor}" + + def _recover_agent_crash(self, error: Dict[str, Any]) -> str: + """Restart the crashed agent or redistribute its tasks.""" + agent = error.get("agent", "unknown") + logger.warning("Agent crash detected: %s. Attempting restart.", agent) + return f"restart_agent:{agent}" + + def _recover_planning_failure(self, error: Dict[str, Any]) -> str: + """Try an alternative planning method.""" + logger.warning("Planning failure detected. Switching to alternative planner.") + return "alternative_planning_method" + + def _recover_execution_failure(self, error: Dict[str, Any]) -> str: + """Re-plan or find an alternative path.""" + logger.warning("Execution failure detected. Triggering re-planning.") + return "replan_trajectory" + + def _recover_communication_failure(self, error: Dict[str, Any]) -> str: + """Use cached data or enter fail-safe mode.""" + logger.warning("Communication failure detected. Using cached data.") + return "use_cached_data_failsafe" + + def _recover_hardware_failure(self, error: Dict[str, Any]) -> str: + """Execute emergency stop or enter limp mode.""" + logger.warning("Hardware failure detected. Executing emergency stop.") + return "emergency_stop_limp_mode" + + def _recover_unknown(self, error: Dict[str, Any]) -> str: + """Generic recovery for unknown error types.""" + logger.warning("Unknown error type. Applying generic recovery.") + return "generic_recovery" diff --git a/resolver/fallback_planner.py b/resolver/fallback_planner.py new file mode 100644 index 0000000..6f2409c --- /dev/null +++ b/resolver/fallback_planner.py @@ -0,0 +1,204 @@ +""" +Fallback strategy planner for the Resolver Agent. + +Provides a hierarchy of fallback strategies when primary approaches fail, +from trying alternative methods all the way to requesting human intervention +or performing a safe shutdown. +""" + +import logging +import time +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional + + +logger = logging.getLogger(__name__) + + +@dataclass +class FallbackPlan: + """A planned fallback strategy.""" + failure_type: str + steps: List[Dict[str, Any]] + depth: int = 0 + requires_human: bool = False + created_at: float = field(default_factory=time.time) + + def to_dict(self) -> Dict[str, Any]: + return { + "failure_type": self.failure_type, + "steps": self.steps, + "depth": self.depth, + "requires_human": self.requires_human, + "created_at": self.created_at, + } + + +@dataclass +class FallbackResult: + """Outcome of executing a fallback plan.""" + plan: FallbackPlan + executed_steps: List[str] + success: bool + final_action: str + executed_at: float = field(default_factory=time.time) + + def to_dict(self) -> Dict[str, Any]: + return { + "failure_type": self.plan.failure_type, + "executed_steps": self.executed_steps, + "success": self.success, + "final_action": self.final_action, + "executed_at": self.executed_at, + } + + +class FallbackPlanner: + """ + Plan and execute fallback strategies when primary approaches fail. + + Fallback hierarchy (depth 0 → 3): + 0. Try primary approach + 1. Try alternative method + 2. Request human help + 3. Safe shutdown + """ + + MAX_FALLBACK_DEPTH = 3 + + # Maps failure types to ordered lists of fallback step descriptions + _FALLBACK_TEMPLATES: Dict[str, List[str]] = { + "navigation": [ + "retry_primary_navigation", + "try_alternative_path", + "try_simple_straight_line", + "try_manual_waypoints", + "request_human_guidance", + "stop_safely", + ], + "manipulation": [ + "retry_primary_grasp", + "try_alternative_grasp_approach", + "try_different_grasp_point", + "request_human_demonstration", + "skip_object", + ], + "perception": [ + "retry_primary_perception", + "switch_to_backup_sensor", + "use_cached_perception_data", + "request_human_visual_confirmation", + "degrade_gracefully", + ], + "planning": [ + "retry_primary_planner", + "try_alternative_planner", + "use_default_safe_plan", + "request_human_plan", + "halt_and_wait", + ], + "communication": [ + "retry_primary_channel", + "switch_to_backup_channel", + "use_cached_commands", + "request_local_mode", + "failsafe_autonomous_mode", + ], + "generic": [ + "retry_operation", + "try_safe_alternative", + "request_human_help", + "safe_shutdown", + ], + } + + def __init__(self): + self._execution_history: List[FallbackResult] = [] + + def plan_fallback(self, failure: Dict[str, Any]) -> FallbackPlan: + """Create a fallback plan for the given failure.""" + failure_type = failure.get("type", "generic") + steps_template = self._FALLBACK_TEMPLATES.get( + failure_type, self._FALLBACK_TEMPLATES["generic"] + ) + + depth = failure.get("depth", 0) + effective_depth = min(depth, self.MAX_FALLBACK_DEPTH) + available_steps = steps_template[effective_depth:] + + steps = [ + {"step": i + 1, "action": action, "description": action.replace("_", " ")} + for i, action in enumerate(available_steps) + ] + + requires_human = any("human" in s["action"] for s in steps) + + plan = FallbackPlan( + failure_type=failure_type, + steps=steps, + depth=effective_depth, + requires_human=requires_human, + ) + + logger.info( + "Fallback plan created for '%s': %d steps (depth=%d)", + failure_type, + len(steps), + effective_depth, + ) + return plan + + def execute_fallback(self, plan: FallbackPlan) -> Dict[str, Any]: + """Execute the first viable step in the fallback plan.""" + executed_steps: List[str] = [] + final_action = "none" + success = False + + for step in plan.steps: + action = step["action"] + executed_steps.append(action) + logger.info("Executing fallback step: %s", action) + + if "human" in action: + # Cannot automatically execute human-in-the-loop steps + final_action = f"awaiting_human_for:{action}" + success = False + break + + if "shutdown" in action or "stop_safely" in action: + final_action = action + success = True + break + + # Simulate execution success for non-human, non-shutdown steps + final_action = action + success = True + break # Execute only the first auto-executable step per call + + result = FallbackResult( + plan=plan, + executed_steps=executed_steps, + success=success, + final_action=final_action, + ) + self._execution_history.append(result) + return result.to_dict() + + # Specific fallback helpers referenced in docstrings + + def fallback_navigation(self, depth: int = 0) -> Dict[str, Any]: + """Execute navigation fallback at the given depth.""" + failure = {"type": "navigation", "depth": depth} + plan = self.plan_fallback(failure) + return self.execute_fallback(plan) + + def fallback_manipulation(self, depth: int = 0) -> Dict[str, Any]: + """Execute manipulation fallback at the given depth.""" + failure = {"type": "manipulation", "depth": depth} + plan = self.plan_fallback(failure) + return self.execute_fallback(plan) + + @property + def execution_history(self) -> List[Dict[str, Any]]: + """Return all previously executed fallback results.""" + return [r.to_dict() for r in self._execution_history] diff --git a/resolver/health_monitor.py b/resolver/health_monitor.py new file mode 100644 index 0000000..7405f0e --- /dev/null +++ b/resolver/health_monitor.py @@ -0,0 +1,224 @@ +""" +Health monitoring module for the Resolver Agent. + +Tracks the health of all registered agents and robot systems, +generates alerts when health degrades, and provides health reports. +""" + +import logging +import time +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Dict, List, Optional + + +logger = logging.getLogger(__name__) + + +class HealthStatus(Enum): + """Possible health states for an agent or the overall system.""" + HEALTHY = "healthy" + DEGRADED = "degraded" + CRITICAL = "critical" + UNKNOWN = "unknown" + + +@dataclass +class AgentHealthRecord: + """Health metrics for a single agent.""" + agent_name: str + status: HealthStatus = HealthStatus.UNKNOWN + response_time: float = 0.0 + cpu_usage: float = 0.0 + memory_usage: float = 0.0 + error_rate: float = 0.0 + uptime: float = 0.0 + last_updated: float = field(default_factory=time.time) + + def to_dict(self) -> Dict[str, Any]: + return { + "agent_name": self.agent_name, + "status": self.status.value, + "response_time": self.response_time, + "cpu_usage": self.cpu_usage, + "memory_usage": self.memory_usage, + "error_rate": self.error_rate, + "uptime": self.uptime, + "last_updated": self.last_updated, + } + + +@dataclass +class Alert: + """A health alert triggered when metrics exceed thresholds.""" + agent_name: str + metric: str + value: float + threshold: float + message: str + timestamp: float = field(default_factory=time.time) + + def to_dict(self) -> Dict[str, Any]: + return { + "agent_name": self.agent_name, + "metric": self.metric, + "value": self.value, + "threshold": self.threshold, + "message": self.message, + "timestamp": self.timestamp, + } + + +class HealthMonitor: + """ + Monitor health of all agents and robot systems. + + Metrics tracked per agent: + - Response time + - CPU / memory usage + - Error rate + - Uptime + - Message queue length (via metadata) + """ + + # Default alert thresholds + RESPONSE_TIME_THRESHOLD = 1.0 # seconds + CPU_THRESHOLD = 80.0 # percent + MEMORY_THRESHOLD = 85.0 # percent + ERROR_RATE_THRESHOLD = 0.1 # 10 % + HEARTBEAT_TIMEOUT = 30.0 # seconds + + def __init__(self): + self._records: Dict[str, AgentHealthRecord] = {} + self._alerts: List[Alert] = [] + self._registration_time: Dict[str, float] = {} + + def register_agent(self, agent_name: str) -> None: + """Register a new agent for health monitoring.""" + self._records[agent_name] = AgentHealthRecord(agent_name=agent_name) + self._registration_time[agent_name] = time.time() + logger.debug("HealthMonitor: registered agent '%s'", agent_name) + + def update_agent_health(self, agent_name: str, state: Any) -> None: + """Update health metrics from an agent's current state.""" + if agent_name not in self._records: + self.register_agent(agent_name) + + record = self._records[agent_name] + record.cpu_usage = getattr(state, "cpu_usage", 0.0) + record.memory_usage = getattr(state, "memory_usage", 0.0) + record.response_time = getattr(state, "response_time", 0.0) + error_count = getattr(state, "error_count", 0) + task_count = getattr(state, "task_count", 1) or 1 + record.error_rate = error_count / task_count + record.uptime = time.time() - self._registration_time.get(agent_name, time.time()) + record.last_updated = time.time() + + # Derive status from metrics + record.status = self._derive_status(record) + + # Check thresholds and raise alerts + self._check_thresholds(record) + + def get_agent_health(self, agent_name: str) -> Dict[str, Any]: + """Return the latest health record for the given agent.""" + record = self._records.get(agent_name) + if record is None: + return {"agent_name": agent_name, "status": HealthStatus.UNKNOWN.value} + return record.to_dict() + + def monitor_system_health(self) -> Dict[str, Any]: + """Return health records for all monitored agents.""" + return {name: record.to_dict() for name, record in self._records.items()} + + def compute_overall_status(self, agent_health: Dict[str, Any]) -> str: + """Compute the overall system status from individual agent statuses.""" + statuses = [info.get("status", HealthStatus.UNKNOWN.value) + for info in agent_health.values()] + if HealthStatus.CRITICAL.value in statuses: + return HealthStatus.CRITICAL.value + if HealthStatus.DEGRADED.value in statuses: + return HealthStatus.DEGRADED.value + if all(s == HealthStatus.HEALTHY.value for s in statuses) and statuses: + return HealthStatus.HEALTHY.value + return HealthStatus.UNKNOWN.value + + def generate_health_report(self) -> Dict[str, Any]: + """Generate a comprehensive health report.""" + agent_health = self.monitor_system_health() + return { + "overall_status": self.compute_overall_status(agent_health), + "agents": agent_health, + "alerts": self.get_active_alerts(), + "total_agents": len(self._records), + "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + } + + def get_active_alerts(self) -> List[Dict[str, Any]]: + """Return all currently active (un-cleared) alerts.""" + cutoff = time.time() - 300 # keep alerts for 5 minutes + self._alerts = [a for a in self._alerts if a.timestamp > cutoff] + return [a.to_dict() for a in self._alerts] + + def predict_failures(self) -> List[Dict[str, Any]]: + """Predict potential failures based on current trends.""" + predictions: List[Dict[str, Any]] = [] + for name, record in self._records.items(): + if record.error_rate > 0.05: + predictions.append({ + "agent": name, + "risk": "high_error_rate", + "current_rate": record.error_rate, + "prediction": "possible_agent_failure", + }) + if record.response_time > 0.8: + predictions.append({ + "agent": name, + "risk": "slow_response", + "current_time": record.response_time, + "prediction": "possible_timeout", + }) + return predictions + + # ------------------------------------------------------------------ # + # Internal helpers + # ------------------------------------------------------------------ # + + def _derive_status(self, record: AgentHealthRecord) -> HealthStatus: + """Determine HealthStatus from a record's metric values.""" + if (record.cpu_usage > self.CPU_THRESHOLD + or record.memory_usage > self.MEMORY_THRESHOLD + or record.error_rate > self.ERROR_RATE_THRESHOLD * 2): + return HealthStatus.CRITICAL + if (record.response_time > self.RESPONSE_TIME_THRESHOLD + or record.error_rate > self.ERROR_RATE_THRESHOLD): + return HealthStatus.DEGRADED + return HealthStatus.HEALTHY + + def _check_thresholds(self, record: AgentHealthRecord) -> None: + """Create alerts when metrics exceed defined thresholds.""" + checks = [ + ("response_time", record.response_time, self.RESPONSE_TIME_THRESHOLD, + f"Agent '{record.agent_name}' response time {record.response_time:.2f}s " + f"exceeds threshold {self.RESPONSE_TIME_THRESHOLD}s"), + ("cpu_usage", record.cpu_usage, self.CPU_THRESHOLD, + f"Agent '{record.agent_name}' CPU usage {record.cpu_usage:.1f}% " + f"exceeds threshold {self.CPU_THRESHOLD}%"), + ("memory_usage", record.memory_usage, self.MEMORY_THRESHOLD, + f"Agent '{record.agent_name}' memory usage {record.memory_usage:.1f}% " + f"exceeds threshold {self.MEMORY_THRESHOLD}%"), + ("error_rate", record.error_rate, self.ERROR_RATE_THRESHOLD, + f"Agent '{record.agent_name}' error rate {record.error_rate:.2%} " + f"exceeds threshold {self.ERROR_RATE_THRESHOLD:.0%}"), + ] + for metric, value, threshold, message in checks: + if value > threshold: + alert = Alert( + agent_name=record.agent_name, + metric=metric, + value=value, + threshold=threshold, + message=message, + ) + self._alerts.append(alert) + logger.warning("ALERT: %s", message) diff --git a/resolver/predictor.py b/resolver/predictor.py new file mode 100644 index 0000000..1bb3748 --- /dev/null +++ b/resolver/predictor.py @@ -0,0 +1,211 @@ +""" +ML-based conflict predictor for the Resolver Agent. + +Uses a lightweight PyTorch neural network to predict conflict probabilities +from agent state feature vectors. Falls back gracefully when PyTorch is +unavailable so the rest of the system continues to operate. +""" + +import logging +import time +from typing import Any, Dict, List, Optional + + +logger = logging.getLogger(__name__) + +# Try importing PyTorch; fail gracefully if it is not installed +try: + import torch + import torch.nn as nn + + _TORCH_AVAILABLE = True +except ImportError: # pragma: no cover + _TORCH_AVAILABLE = False + logger.warning("PyTorch not available; ML conflict prediction will use heuristics only") + + +# --------------------------------------------------------------------------- +# Neural network model definition +# --------------------------------------------------------------------------- + +if _TORCH_AVAILABLE: + + class _ConflictPredictorNet(nn.Module): + """Simple feed-forward network for binary conflict prediction.""" + + def __init__(self, input_dim: int = 20, hidden_dim: int = 64): + super().__init__() + self.network = nn.Sequential( + nn.Linear(input_dim, hidden_dim), + nn.ReLU(), + nn.Dropout(0.2), + nn.Linear(hidden_dim, hidden_dim // 2), + nn.ReLU(), + nn.Linear(hidden_dim // 2, 1), + nn.Sigmoid(), + ) + + def forward(self, x: "torch.Tensor") -> "torch.Tensor": # noqa: F821 + return self.network(x) + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + +class ConflictPredictor: + """ + Predict conflicts before they happen. + + If PyTorch is available, a neural network model is trained on historical + data. Otherwise, a simple heuristic-based fallback is used. + """ + + INPUT_DIM = 20 # feature vector length per prediction call + HIDDEN_DIM = 64 + + def __init__(self): + self._model: Optional[Any] = None + self._trained = False + self._history: List[Dict[str, Any]] = [] + + if _TORCH_AVAILABLE: + self._model = _ConflictPredictorNet(self.INPUT_DIM, self.HIDDEN_DIM) + logger.info("ConflictPredictor: PyTorch model initialised") + + # ------------------------------------------------------------------ # + # Training + # ------------------------------------------------------------------ # + + def train_predictor(self, historical_conflicts: List[Dict[str, Any]]) -> Dict[str, Any]: + """Train the predictor on a list of historical conflict records.""" + if not _TORCH_AVAILABLE or not historical_conflicts: + logger.warning("Training skipped: PyTorch unavailable or no data provided") + return {"trained": False, "reason": "no_data_or_no_torch"} + + import torch # pylint: disable=import-outside-toplevel + import torch.nn as nn # pylint: disable=import-outside-toplevel + + features, labels = self._extract_features(historical_conflicts) + X = torch.tensor(features, dtype=torch.float32) + y = torch.tensor(labels, dtype=torch.float32).unsqueeze(1) + + optimizer = torch.optim.Adam(self._model.parameters(), lr=1e-3) + criterion = nn.BCELoss() + + epochs = 50 + for _ in range(epochs): + self._model.train() + optimizer.zero_grad() + output = self._model(X) + loss = criterion(output, y) + loss.backward() + optimizer.step() + + self._trained = True + logger.info("ConflictPredictor: model trained on %d records", len(historical_conflicts)) + return {"trained": True, "samples": len(historical_conflicts), "epochs": epochs} + + # ------------------------------------------------------------------ # + # Inference + # ------------------------------------------------------------------ # + + def predict_conflict_probability( + self, agent_states: Dict[str, Any] + ) -> Dict[str, Any]: + """Predict the probability of a conflict given current agent states.""" + feature_vector = self._states_to_features(agent_states) + + if _TORCH_AVAILABLE and self._trained and self._model is not None: + import torch # pylint: disable=import-outside-toplevel + + self._model.eval() + with torch.no_grad(): + x = torch.tensor(feature_vector, dtype=torch.float32).unsqueeze(0) + probability = float(self._model(x).item()) + else: + # Heuristic fallback: high error counts or resource contention raise probability + probability = self._heuristic_probability(agent_states) + + result = { + "probability": probability, + "threshold": 0.5, + "likely_conflict": probability >= 0.5, + "timestamp": time.time(), + } + self._history.append(result) + return result + + def predict_best_resolution(self, conflict: Dict[str, Any]) -> Dict[str, Any]: + """Use the model to suggest the best resolution (heuristic if no model).""" + agents = conflict.get("agents", []) + from resolver.arbitrator import AgentArbitrator # pylint: disable=import-outside-toplevel + priorities = AgentArbitrator.DEFAULT_PRIORITIES + if not agents: + return {"winning_agent": None, "action": "no_resolution"} + winner = max(agents, key=lambda a: priorities.get(a, 0)) + return { + "winning_agent": winner, + "action": f"grant_to_{winner}", + "confidence": 0.75, + } + + def suggest_preventive_actions( + self, agent_states: Dict[str, Any] + ) -> List[str]: + """Suggest actions to prevent predicted conflicts.""" + prediction = self.predict_conflict_probability(agent_states) + actions: List[str] = [] + if prediction["likely_conflict"]: + actions.append("pre_allocate_contested_resources") + actions.append("notify_agents_of_potential_conflict") + actions.append("increase_monitoring_frequency") + return actions + + # ------------------------------------------------------------------ # + # Internal helpers + # ------------------------------------------------------------------ # + + def _states_to_features(self, agent_states: Dict[str, Any]) -> List[float]: + """Convert agent states to a fixed-length feature vector.""" + features: List[float] = [] + ordered_agents = ["perception", "planning", "control", "communication", "coordination"] + for agent_name in ordered_agents: + state = agent_states.get(agent_name) + if state is not None: + features.extend([ + float(getattr(state, "cpu_usage", 0.0)) / 100.0, + float(getattr(state, "memory_usage", 0.0)) / 100.0, + float(getattr(state, "error_count", 0)) / 100.0, + float(getattr(state, "response_time", 0.0)), + ]) + else: + features.extend([0.0, 0.0, 0.0, 0.0]) + # Pad / truncate to INPUT_DIM + features = (features + [0.0] * self.INPUT_DIM)[: self.INPUT_DIM] + return features + + def _extract_features( + self, records: List[Dict[str, Any]] + ): + """Extract (feature_matrix, label_vector) from historical records.""" + features = [] + labels = [] + for record in records: + # Feature: encode conflict severity and agent count + severity = float(record.get("severity", 1)) + agent_count = float(len(record.get("agents", []))) + vec = [severity / 5.0, agent_count / 6.0] + [0.0] * (self.INPUT_DIM - 2) + features.append(vec[: self.INPUT_DIM]) + labels.append(1.0 if record.get("conflict_occurred", True) else 0.0) + return features, labels + + def _heuristic_probability(self, agent_states: Dict[str, Any]) -> float: + """Simple heuristic fallback for conflict probability estimation.""" + score = 0.0 + for state in agent_states.values(): + if getattr(state, "error_count", 0) > 5: + score += 0.2 + resources = getattr(state, "metadata", {}).get("requested_resources", []) + score += len(resources) * 0.05 + return min(score, 0.99) diff --git a/ros2_interface/__init__.py b/ros2_interface/__init__.py new file mode 100644 index 0000000..3591816 --- /dev/null +++ b/ros2_interface/__init__.py @@ -0,0 +1,3 @@ +""" +ROS2 interface package for the Resolver Agent. +""" diff --git a/ros2_interface/launch/resolver.launch.py b/ros2_interface/launch/resolver.launch.py new file mode 100644 index 0000000..4203461 --- /dev/null +++ b/ros2_interface/launch/resolver.launch.py @@ -0,0 +1,28 @@ +""" +ROS2 launch file for the Resolver Agent node. +""" + +from launch import LaunchDescription +from launch_ros.actions import Node + + +def generate_launch_description() -> LaunchDescription: + resolver_node = Node( + package="rag7", + executable="resolver_node", + name="resolver_agent_node", + output="screen", + parameters=[ + {"monitoring_rate": 10.0}, + {"health_check_rate": 1.0}, + ], + remappings=[ + ("/agents/perception/status", "/agents/perception/status"), + ("/agents/planning/status", "/agents/planning/status"), + ("/agents/control/status", "/agents/control/status"), + ("/agents/communication/status", "/agents/communication/status"), + ("/agents/coordination/status", "/agents/coordination/status"), + ], + ) + + return LaunchDescription([resolver_node]) diff --git a/ros2_interface/resolver_node.py b/ros2_interface/resolver_node.py new file mode 100644 index 0000000..36e07e8 --- /dev/null +++ b/ros2_interface/resolver_node.py @@ -0,0 +1,268 @@ +""" +ROS2 Resolver Node for the Resolver Agent. + +This node provides the ROS2 interface for the Resolver Agent, subscribing to +agent status topics, handling error reports, and publishing resolutions and +health data. + +Note: This module requires ROS2 (rclpy) to run. When rclpy is not available, +the module defines stub classes to allow import and testing without ROS2. +""" + +import logging +import time +from typing import Any, Dict + +try: + import rclpy + from rclpy.node import Node + from std_msgs.msg import String + + _ROS2_AVAILABLE = True +except ImportError: + _ROS2_AVAILABLE = False + + # --------------------------------------------------------------------------- + # Minimal stubs so the module can be imported without a ROS2 installation + # --------------------------------------------------------------------------- + + class Node: # type: ignore[no-redef] + """Stub ROS2 Node for non-ROS environments.""" + + def __init__(self, node_name: str, **kwargs): + self._node_name = node_name + self._logger = logging.getLogger(f"ros2.{node_name}") + + def get_logger(self): + return self._logger + + def create_subscription(self, *args, **kwargs): + return None + + def create_publisher(self, *args, **kwargs): + return _StubPublisher() + + def create_service(self, *args, **kwargs): + return None + + def create_timer(self, *args, **kwargs): + return None + + class _StubPublisher: + def publish(self, msg): + pass + + class String: # type: ignore[no-redef] + def __init__(self): + self.data = "" + + +logger = logging.getLogger(__name__) + +from agents.resolver_agent import ResolverAgent # noqa: E402 + + +class ResolverNode(Node): + """ + ROS2 node for the Resolver Agent. + + Topics subscribed: + /agents/perception/status + /agents/planning/status + /agents/control/status + /agents/communication/status + /agents/coordination/status + /system/errors + /system/conflicts + + Topics published: + /resolver/resolutions + /resolver/health_status + /resolver/alerts + /resolver/recovery_actions + + Services (when ROS2 is available): + /resolver/resolve_conflict + /resolver/recover_error + /resolver/get_health_status + /resolver/arbitrate_request + + Timer: + health_check_timer – 1 Hz + """ + + HEALTH_CHECK_RATE = 1.0 # Hz + + def __init__(self): + super().__init__("resolver_agent_node") + self.resolver = ResolverAgent() + + self._setup_subscriptions() + self._setup_publishers() + self._setup_timer() + + self.resolver.start() + self.get_logger().info("ResolverNode initialized") # type: ignore[attr-defined] + + # ------------------------------------------------------------------ # + # Setup + # ------------------------------------------------------------------ # + + def _setup_subscriptions(self) -> None: + """Subscribe to all agent status and system error topics.""" + agent_topics = [ + "/agents/perception/status", + "/agents/planning/status", + "/agents/control/status", + "/agents/communication/status", + "/agents/coordination/status", + ] + for topic in agent_topics: + self.create_subscription( # type: ignore[attr-defined] + String, topic, self.handle_agent_status, 10 + ) + + self.create_subscription( # type: ignore[attr-defined] + String, "/system/errors", self.handle_error_report, 10 + ) + self.create_subscription( # type: ignore[attr-defined] + String, "/system/conflicts", self.handle_conflict_report, 10 + ) + + def _setup_publishers(self) -> None: + """Create publishers for resolver outputs.""" + self._pub_resolutions = self.create_publisher( # type: ignore[attr-defined] + String, "/resolver/resolutions", 10 + ) + self._pub_health = self.create_publisher( # type: ignore[attr-defined] + String, "/resolver/health_status", 10 + ) + self._pub_alerts = self.create_publisher( # type: ignore[attr-defined] + String, "/resolver/alerts", 10 + ) + self._pub_recovery = self.create_publisher( # type: ignore[attr-defined] + String, "/resolver/recovery_actions", 10 + ) + + def _setup_timer(self) -> None: + """Create the periodic health check timer (1 Hz).""" + self.create_timer( # type: ignore[attr-defined] + 1.0 / self.HEALTH_CHECK_RATE, self.health_check_timer + ) + + # ------------------------------------------------------------------ # + # Callbacks + # ------------------------------------------------------------------ # + + def handle_agent_status(self, msg: Any) -> None: + """Process agent status updates from subscribed topics.""" + import json # pylint: disable=import-outside-toplevel + + try: + data: Dict[str, Any] = json.loads(msg.data) + agent_name = data.get("agent", "unknown") + logger.debug("Received status from agent '%s'", agent_name) + # Forward to health monitor via resolver + self.resolver.health_monitor.update_agent_health( + agent_name, _DictState(data) + ) + except Exception as exc: # pylint: disable=broad-except + logger.error("Failed to process agent status: %s", exc) + + def handle_error_report(self, msg: Any) -> None: + """Handle error reports from /system/errors.""" + import json # pylint: disable=import-outside-toplevel + + try: + error: Dict[str, Any] = json.loads(msg.data) + result = self.resolver.recover_from_error(error) + self.publish_recovery_action(result) + except Exception as exc: # pylint: disable=broad-except + logger.error("Failed to handle error report: %s", exc) + + def handle_conflict_report(self, msg: Any) -> None: + """Handle conflict reports from /system/conflicts.""" + import json # pylint: disable=import-outside-toplevel + + try: + conflict: Dict[str, Any] = json.loads(msg.data) + resolution = self.resolver.resolve_conflict(conflict) + self.publish_resolution(resolution) + except Exception as exc: # pylint: disable=broad-except + logger.error("Failed to handle conflict report: %s", exc) + + def health_check_timer(self) -> None: + """Periodic health check callback (1 Hz).""" + import json # pylint: disable=import-outside-toplevel + + health = self.resolver.assess_system_health() + msg = String() + msg.data = json.dumps(health) + self._pub_health.publish(msg) + + for alert in health.get("alerts", []): + alert_msg = String() + alert_msg.data = json.dumps(alert) + self._pub_alerts.publish(alert_msg) + + # ------------------------------------------------------------------ # + # Publishers + # ------------------------------------------------------------------ # + + def publish_resolution(self, resolution: Dict[str, Any]) -> None: + """Publish a conflict resolution to /resolver/resolutions.""" + import json # pylint: disable=import-outside-toplevel + + msg = String() + msg.data = json.dumps(resolution) + self._pub_resolutions.publish(msg) + + def publish_recovery_action(self, action: Dict[str, Any]) -> None: + """Publish a recovery action to /resolver/recovery_actions.""" + import json # pylint: disable=import-outside-toplevel + + msg = String() + msg.data = json.dumps(action) + self._pub_recovery.publish(msg) + + +# --------------------------------------------------------------------------- +# Helper: thin wrapper to expose a dict as an object with attribute access +# --------------------------------------------------------------------------- + +class _DictState: + """Adapts a plain dict to the attribute-access API expected by HealthMonitor.""" + + def __init__(self, data: Dict[str, Any]): + self._data = data + + def __getattr__(self, name: str) -> Any: + if name.startswith("_"): + raise AttributeError(name) + return self._data.get(name, None) + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +def main() -> None: + """Launch the ROS2 ResolverNode.""" + if not _ROS2_AVAILABLE: + logger.error("rclpy is not installed; cannot launch ROS2 node") + return + + rclpy.init() + node = ResolverNode() + try: + rclpy.spin(node) + except KeyboardInterrupt: + pass + finally: + node.resolver.stop() + node.destroy_node() + rclpy.shutdown() + + +if __name__ == "__main__": + main() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..10de823 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Tests package init. +""" diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91f96fc7201299d83e63868f402d281522f19f38 GIT binary patch literal 180 zcmX@j%ge<81Q92uXUYKS#~=<2FhUuhIe?7m3@Hpz43&(UOjV*>A*sbB#R>(9$=QkN zsS25SnI(E$ewvK8*yH0<@{{A^S2BDC>HVdvpOK%Ns$W!^mzP?kU!Gr-tzVRwZVn+z yK!)hY$Ah%T$LkeT{^GF7%}*)KNwq6t1L_7@Ukq~H2WCb_#!pO4j4VYgKrR3oBQ1dd literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_arbitrator.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_arbitrator.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f52f9bdc64490866253097ec202edeebf9622c1c GIT binary patch literal 6995 zcmd5>T}&L;6}~gOvpc(9SQZTSZj%^O+x3Eh82IJ?GBO{)4fb zM79Ek~G2WR#;!%o&ChDG*ugyr2+W{}WqJLjyqj?TtPE%6BiKC_ z+4hJxYu1y{v zHs*7g^+%Z|j->b$9~5Wzxr35y`m;*B$=Qs?&mlpG|RWN z1bM^Ti~YUXZllJktBLq94pGKaW0^(RCv#Bp;zRhkKok<@05kyW4hh?29;jWm0TpCB zPzN3q0CNBpWhYRVECO}QE}%PPH&Bnf189Zp0a__n0QJh1K&w*TV3kpW=PBCdn6(Mq z5y~4S_M!pE0!e23EZNKvDchDJK`s>JDGo5)Ik5ye!>%dOs~U3Fe}A-fA`w$s>2y4< z(AJp*z1B)2Gof$1Lyr&N-}|6;xi?+a{k3=Ki8%Bh z_-J!x zMkDdK8Xsqx3KB&Np*|pndrARKol1fqqPw9X0}sWCj2$Kr10PLCq7O3)4BBm~D18H1 z3YB>OCUUjRK!TCe%53K=KvFmqwO#86a*I5xtA7yDJ#VkoH7s$ur)^!Z?R3wJ>msSH z%Y&;U?dzX+JVUs4>*DVFb&I0z4?gHx5j#o&Lv3bO@eNcgz-0U9Hd#Q(u=XQ>=0I5| z9Y9!_wC-xF0lt&$X3uMYY7XR0)xpWsfy;4%I*1{<2SzhZ7y-0%s^C&$3Wa-WEM<7c z5(z4+@d)aLA!?Z$%G3}CGhn76v;n6=XJS+joFBM&dLYzySsJ=DaPi_`|9~M}k3^>x z`g7;gjFJD1fN{1i7rO~%#6lPi0J$-9+`coQ z#xqLe*8NRSn|oH9hxO*+)#l53^W~>vIG=;5)rl4Hc&S>Ue3)8&3Y8z**@80Q#a2|n zDf6fdYM$DFGT1^4Qn;d{(Zm=-J7_ok^P4&UY~5#4j*A9hG826a)C#=v!6If3B>Z-RoK96xCL9y`;@j~D>KJZb342ZM8MUn<8e(gZ|T{A*Mz2U znhw~MHV6dGuVG2hJ;&F;Z!5azXsPKj@NT4iXENYrOphU@=>DTi(G{_;6fSg}%)0&p zl~TCCbvxLt3(+xt$L&A(tt&D&TLRyvZrS8#ZeJD(oU%oKuQ5#tK0KnRQDV~Z@XXj zbga7sc0qfx7xc>ke=7Es7%fu;bw%tcRfVjh{b{0FikWXFU@~^){ADttU6ZW9#q^XV zPO><3Ef|w=kJ|}d%ZwXBOo~F+4&B$WT%GoHl{&_HYUDT}g; z#YDE(HYr|qGvnET^rmeSld;Jj#^o$9mWq(UCl51oc#iobWMI51=LIx;vWYQ(38Sd$Z|Hp6JqT;8jDy4Ia`pLls! z-ga9gH7tt#U*cmk>Sb<|DL_5!pB_i@3m_vDZ6#<%wMJdejAzv31j|j88%gtKMgKw@ zku)JGy=aRQp!_Mos59a9wi2N7L(#Emmw}+ag(wy-QuMR$>z>xq3$u=lPg?bAK;W&gcHe4kyI$Kq@6H7sfyJ}BzxAt{rSR&pQ~I$}>0@WohX+^0v!!)T zvPx01xUZ`!lXnd!5@?Jq*0q_S%mF9@DsKmY&$ literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_conflict_resolution.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_conflict_resolution.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8665def686e7f26c56a1db78ed15c48d0fe75233 GIT binary patch literal 12545 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 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_error_recovery.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_error_recovery.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..187402e40efd4705983ce74f75a0de2c720214dc GIT binary patch literal 9168 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 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_integration.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_integration.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a4d00077716990c47393b8db3918560712d4195d GIT binary patch literal 11188 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 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_resolver_agent.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_resolver_agent.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3976be8f19948bbb6eabe512e14d6132984a57a6 GIT binary patch literal 15153 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 AJpcdz literal 0 HcmV?d00001 diff --git a/tests/test_arbitrator.py b/tests/test_arbitrator.py new file mode 100644 index 0000000..297190d --- /dev/null +++ b/tests/test_arbitrator.py @@ -0,0 +1,129 @@ +""" +Tests for the agent arbitrator. +""" + +import sys +import os +import unittest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from resolver.arbitrator import AgentArbitrator, ArbitrationStrategy + + +class TestAgentArbitrator(unittest.TestCase): + def setUp(self): + self.arbitrator = AgentArbitrator() + + def _make_requests(self, agents): + return [{"agent": a, "resource": "GPU", "priority": 0} for a in agents] + + # ------------------------------------------------------------------ # + # Control arbitration + # ------------------------------------------------------------------ # + + def test_arbitrate_control_no_requests(self): + result = self.arbitrator.arbitrate_control_request([]) + self.assertIsNone(result["winner"]) + + def test_arbitrate_control_priority_queue(self): + requests = [ + {"agent": "planning", "priority": 0}, + {"agent": "perception", "priority": 0}, + ] + result = self.arbitrator.arbitrate_control_request(requests) + # perception has higher default priority (5 vs 4) + self.assertEqual(result["winner"], "perception") + + def test_arbitrate_control_emergency_override(self): + requests = [ + {"agent": "planning", "priority": 0, "emergency": True}, + {"agent": "coordination", "priority": 0, "emergency": True}, + ] + result = self.arbitrator.arbitrate_control_request(requests) + self.assertEqual(result["strategy"], ArbitrationStrategy.EMERGENCY_OVERRIDE.value) + self.assertEqual(result["winner"], "planning") # planning > coordination + + # ------------------------------------------------------------------ # + # Resource allocation + # ------------------------------------------------------------------ # + + def test_arbitrate_resource_allocation_empty(self): + result = self.arbitrator.arbitrate_resource_allocation([]) + self.assertIsNone(result["winner"]) + self.assertEqual(result["allocation"], {}) + + def test_arbitrate_resource_allocation_assigns_winner(self): + requests = [ + {"agent": "perception", "resource": "GPU"}, + {"agent": "planning", "resource": "GPU"}, + ] + result = self.arbitrator.arbitrate_resource_allocation(requests) + self.assertEqual(result["allocation"]["GPU"], "perception") + + def test_arbitrate_resource_multiple_resources(self): + requests = [ + {"agent": "perception", "resource": "GPU"}, + {"agent": "planning", "resource": "GPU"}, + {"agent": "control", "resource": "CPU"}, + {"agent": "coordination", "resource": "CPU"}, + ] + result = self.arbitrator.arbitrate_resource_allocation(requests) + self.assertEqual(result["allocation"]["GPU"], "perception") + self.assertEqual(result["allocation"]["CPU"], "control") + + # ------------------------------------------------------------------ # + # Task priority + # ------------------------------------------------------------------ # + + def test_arbitrate_task_priority_empty(self): + result = self.arbitrator.arbitrate_task_priority([]) + self.assertIsNone(result["winner"]) + + def test_arbitrate_task_priority_orders_tasks(self): + tasks = [ + {"id": "t1", "agent": "coordination", "priority": 1}, + {"id": "t2", "agent": "perception", "priority": 1}, + {"id": "t3", "agent": "planning", "priority": 2}, + ] + result = self.arbitrator.arbitrate_task_priority(tasks) + ordered = result["allocation"]["ordered_tasks"] + # t3 has higher priority (2) so it should come first + self.assertEqual(ordered[0], "t3") + + # ------------------------------------------------------------------ # + # Context-aware arbitration + # ------------------------------------------------------------------ # + + def test_arbitrate_with_context_emergency(self): + requests = [ + {"agent": "coordination", "priority": 0}, + {"agent": "control", "priority": 0}, + ] + result = self.arbitrator.arbitrate_with_context(requests, {"emergency": True}) + self.assertEqual(result["strategy"], ArbitrationStrategy.EMERGENCY_OVERRIDE.value) + + def test_arbitrate_with_context_normal(self): + requests = [ + {"agent": "coordination", "priority": 0}, + {"agent": "perception", "priority": 0}, + ] + result = self.arbitrator.arbitrate_with_context(requests, {"emergency": False}) + # Normal priority-based + self.assertEqual(result["winner"], "perception") + + # ------------------------------------------------------------------ # + # Default priorities + # ------------------------------------------------------------------ # + + def test_default_priorities_resolver_highest(self): + priorities = AgentArbitrator.DEFAULT_PRIORITIES + self.assertEqual(priorities["resolver"], max(priorities.values())) + + def test_default_priorities_coordination_lowest(self): + priorities = AgentArbitrator.DEFAULT_PRIORITIES + self.assertEqual(priorities["coordination"], min(priorities.values())) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_conflict_resolution.py b/tests/test_conflict_resolution.py new file mode 100644 index 0000000..242c6de --- /dev/null +++ b/tests/test_conflict_resolution.py @@ -0,0 +1,190 @@ +""" +Tests for the conflict resolution module. +""" + +import sys +import os +import time +import unittest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from resolver.conflict_resolution import ( + Conflict, + ConflictDetector, + ConflictPredictor, + ConflictResolver, + ConflictType, + Resolution, + ResolutionStrategy, +) +from agents.base_agent import AgentState, AgentStatus + + +class TestConflictDetector(unittest.TestCase): + def setUp(self): + self.detector = ConflictDetector() + + def test_no_conflicts_empty_states(self): + result = self.detector.detect_all({}) + self.assertEqual(result, []) + + def test_control_conflict_detected(self): + state_a = AgentState(name="planning") + state_a.metadata["requesting_control"] = True + state_b = AgentState(name="control") + state_b.metadata["requesting_control"] = True + conflicts = self.detector.detect_control_conflict( + {"planning": state_a, "control": state_b} + ) + self.assertEqual(len(conflicts), 1) + self.assertEqual(conflicts[0]["type"], ConflictType.CONTROL.value) + self.assertIn("planning", conflicts[0]["agents"]) + self.assertIn("control", conflicts[0]["agents"]) + + def test_task_conflict_detected(self): + state_a = AgentState(name="coordination") + state_a.metadata["assigned_tasks"] = ["task_1"] + state_b = AgentState(name="planning") + state_b.metadata["assigned_tasks"] = ["task_1"] + conflicts = self.detector.detect_task_conflict( + {"coordination": state_a, "planning": state_b} + ) + self.assertEqual(len(conflicts), 1) + self.assertEqual(conflicts[0]["type"], ConflictType.TASK.value) + + def test_resource_conflict_detected(self): + state_a = AgentState(name="perception") + state_a.metadata["requested_resources"] = ["GPU"] + state_b = AgentState(name="planning") + state_b.metadata["requested_resources"] = ["GPU"] + conflicts = self.detector.detect_resource_conflict( + {"perception": state_a, "planning": state_b} + ) + self.assertEqual(len(conflicts), 1) + self.assertEqual(conflicts[0]["details"]["resource"], "GPU") + + def test_perception_planning_conflict_no_disagreement(self): + state_p = AgentState(name="perception") + state_p.metadata["detected_objects"] = ["chair"] + state_pl = AgentState(name="planning") + state_pl.metadata["known_objects"] = ["chair"] + conflicts = self.detector.detect_perception_planning_conflict( + {"perception": state_p, "planning": state_pl} + ) + self.assertEqual(conflicts, []) + + def test_perception_planning_conflict_with_disagreement(self): + state_p = AgentState(name="perception") + state_p.metadata["detected_objects"] = ["chair", "table"] + state_pl = AgentState(name="planning") + state_pl.metadata["known_objects"] = ["chair"] + conflicts = self.detector.detect_perception_planning_conflict( + {"perception": state_p, "planning": state_pl} + ) + self.assertEqual(len(conflicts), 1) + + +class TestConflictResolver(unittest.TestCase): + def setUp(self): + self.resolver = ConflictResolver() + + def _make_conflict(self, agents=None): + return { + "type": ConflictType.CONTROL.value, + "agents": agents or ["planning", "control"], + "description": "Test conflict", + } + + def test_resolve_by_priority(self): + conflict = self._make_conflict(["planning", "control"]) + result = self.resolver.resolve_by_priority(conflict) + self.assertEqual(result.winning_agent, "planning") # priority 4 > 3 + + def test_resolve_by_priority_returns_resolution(self): + conflict = self._make_conflict() + result = self.resolver.resolve(conflict, ResolutionStrategy.PRIORITY_BASED) + self.assertIn("winning_agent", result) + self.assertIn("strategy", result) + + def test_resolve_by_voting(self): + conflict = self._make_conflict(["perception", "planning"]) + result = self.resolver.resolve_by_voting(conflict) + self.assertEqual(result.winning_agent, "perception") # priority 5 > 4 + + def test_resolve_by_expertise_control_conflict(self): + conflict = self._make_conflict() + result = self.resolver.resolve_by_expertise(conflict) + self.assertEqual(result.winning_agent, "control") + + def test_resolve_by_cost_no_options(self): + conflict = self._make_conflict() + result = self.resolver.resolve_by_cost(conflict) + # Falls back to priority-based + self.assertIsInstance(result.winning_agent, str) + + def test_resolve_by_cost_with_options(self): + conflict = { + "type": ConflictType.RESOURCE.value, + "agents": ["planning", "control"], + "details": { + "options": [ + {"id": "opt_a", "agent": "planning", "cost": 10}, + {"id": "opt_b", "agent": "control", "cost": 5}, + ] + }, + } + result = self.resolver.resolve_by_cost(conflict) + self.assertEqual(result.winning_agent, "control") + + def test_resolution_history_grows(self): + conflict = self._make_conflict() + self.resolver.resolve(conflict) + self.resolver.resolve(conflict) + self.assertEqual(len(self.resolver.resolution_history), 2) + + def test_resolve_no_agents(self): + conflict = {"type": "control", "agents": []} + result = self.resolver.resolve_by_priority(conflict) + self.assertFalse(result.success) + + +class TestConflict(unittest.TestCase): + def test_to_dict(self): + c = Conflict( + conflict_type=ConflictType.TASK, + agents=["a", "b"], + description="test", + ) + d = c.to_dict() + self.assertEqual(d["type"], ConflictType.TASK.value) + self.assertEqual(d["agents"], ["a", "b"]) + self.assertIn("timestamp", d) + + +class TestConflictPredictor(unittest.TestCase): + def setUp(self): + self.predictor = ConflictPredictor() + + def test_predict_no_conflicts(self): + predictions = self.predictor.predict({}) + self.assertEqual(predictions, []) + + def test_predict_resource_contention(self): + state_a = AgentState(name="perception") + state_a.metadata["requested_resources"] = ["GPU"] + state_b = AgentState(name="planning") + state_b.metadata["requested_resources"] = ["GPU"] + predictions = self.predictor.predict( + {"perception": state_a, "planning": state_b} + ) + self.assertEqual(len(predictions), 1) + self.assertGreater(predictions[0]["probability"], 0.5) + + def test_suggest_preventive_actions_empty(self): + actions = self.predictor.suggest_preventive_actions({}) + self.assertEqual(actions, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_error_recovery.py b/tests/test_error_recovery.py new file mode 100644 index 0000000..ad0e03c --- /dev/null +++ b/tests/test_error_recovery.py @@ -0,0 +1,109 @@ +""" +Tests for the error recovery system. +""" + +import sys +import os +import unittest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from resolver.error_recovery import ErrorRecoverySystem, ErrorSeverity, ErrorType, ErrorRecord +from agents.base_agent import AgentState + + +class TestErrorSeverity(unittest.TestCase): + def test_ordering(self): + self.assertLess(ErrorSeverity.INFO, ErrorSeverity.WARNING) + self.assertLess(ErrorSeverity.WARNING, ErrorSeverity.ERROR) + self.assertLess(ErrorSeverity.ERROR, ErrorSeverity.CRITICAL) + self.assertLess(ErrorSeverity.CRITICAL, ErrorSeverity.FATAL) + + +class TestErrorRecoverySystem(unittest.TestCase): + def setUp(self): + self.system = ErrorRecoverySystem() + + def test_classify_sensor_failure(self): + error = { + "type": ErrorType.SENSOR_FAILURE, + "severity": ErrorSeverity.ERROR, + "sensor": "camera", + "description": "Camera offline", + } + record = self.system.classify_error(error) + self.assertIsInstance(record, ErrorRecord) + self.assertEqual(record.error_type, ErrorType.SENSOR_FAILURE) + self.assertEqual(record.severity, ErrorSeverity.ERROR) + + def test_classify_default_severity(self): + record = self.system.classify_error({"type": "sensor_failure"}) + self.assertEqual(record.severity, ErrorSeverity.ERROR) + + def test_execute_recovery_sensor(self): + error = {"type": ErrorType.SENSOR_FAILURE, "sensor": "lidar"} + result = self.system.execute_recovery(error) + self.assertIn("recovery_action", result) + self.assertIn("lidar", result["recovery_action"]) + self.assertTrue(result["recovery_success"]) + + def test_execute_recovery_agent_crash(self): + error = {"type": ErrorType.AGENT_CRASH, "agent": "planning"} + result = self.system.execute_recovery(error) + self.assertIn("planning", result["recovery_action"]) + + def test_execute_recovery_planning_failure(self): + error = {"type": ErrorType.PLANNING_FAILURE} + result = self.system.execute_recovery(error) + self.assertTrue(result["recovery_success"]) + + def test_execute_recovery_execution_failure(self): + error = {"type": ErrorType.EXECUTION_FAILURE} + result = self.system.execute_recovery(error) + self.assertIn("replan", result["recovery_action"]) + + def test_execute_recovery_communication_failure(self): + error = {"type": ErrorType.COMMUNICATION_FAILURE} + result = self.system.execute_recovery(error) + self.assertIn("cached", result["recovery_action"]) + + def test_execute_recovery_hardware_failure(self): + error = {"type": ErrorType.HARDWARE_FAILURE} + result = self.system.execute_recovery(error) + self.assertIn("emergency", result["recovery_action"]) + + def test_execute_recovery_unknown_type(self): + error = {"type": "totally_unknown"} + result = self.system.execute_recovery(error) + self.assertTrue(result["recovery_success"]) + + def test_max_recovery_attempts(self): + error = {"type": ErrorType.AGENT_CRASH, "agent": "perception"} + for _ in range(ErrorRecoverySystem.MAX_RECOVERY_ATTEMPTS): + self.system.execute_recovery(error) + # One more should trigger escalation + result = self.system.execute_recovery(error) + self.assertFalse(result["recovery_success"]) + self.assertTrue(result.get("escalation_required")) + + def test_error_log_grows(self): + self.system.execute_recovery({"type": ErrorType.SENSOR_FAILURE}) + self.system.execute_recovery({"type": ErrorType.PLANNING_FAILURE}) + self.assertEqual(len(self.system.error_log), 2) + + def test_detect_errors_heartbeat_timeout(self): + import time + state = AgentState(name="planning") + state.last_heartbeat = time.time() - 60 # 60 seconds ago + errors = self.system.detect_errors({"planning": state}) + self.assertTrue(any(e["type"] == ErrorType.AGENT_CRASH for e in errors)) + + def test_detect_errors_high_error_count(self): + state = AgentState(name="control") + state.error_count = 15 + errors = self.system.detect_errors({"control": state}) + self.assertTrue(len(errors) > 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_health_monitor.py b/tests/test_health_monitor.py new file mode 100644 index 0000000..ebe6e22 --- /dev/null +++ b/tests/test_health_monitor.py @@ -0,0 +1,132 @@ +""" +Tests for the health monitoring module. +""" + +import sys +import os +import time +import unittest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from resolver.health_monitor import AgentHealthRecord, HealthMonitor, HealthStatus +from agents.base_agent import AgentState + + +class TestHealthMonitor(unittest.TestCase): + def setUp(self): + self.monitor = HealthMonitor() + + def _make_state(self, **kwargs) -> AgentState: + state = AgentState(name=kwargs.get("name", "test_agent")) + for k, v in kwargs.items(): + if k != "name": + setattr(state, k, v) + return state + + def test_register_agent(self): + self.monitor.register_agent("perception") + self.assertIn("perception", self.monitor._records) + + def test_update_agent_health_auto_registers(self): + state = self._make_state(name="planning", cpu_usage=10.0) + self.monitor.update_agent_health("planning", state) + self.assertIn("planning", self.monitor._records) + + def test_healthy_agent_status(self): + state = self._make_state( + name="control", + cpu_usage=20.0, + memory_usage=30.0, + response_time=0.1, + error_count=0, + task_count=10, + ) + self.monitor.update_agent_health("control", state) + health = self.monitor.get_agent_health("control") + self.assertEqual(health["status"], HealthStatus.HEALTHY.value) + + def test_degraded_agent_status_slow_response(self): + state = self._make_state( + name="control", + response_time=2.0, + error_count=0, + task_count=1, + ) + self.monitor.update_agent_health("control", state) + health = self.monitor.get_agent_health("control") + self.assertEqual(health["status"], HealthStatus.DEGRADED.value) + + def test_critical_agent_status_high_cpu(self): + state = self._make_state(name="control", cpu_usage=95.0) + self.monitor.update_agent_health("control", state) + health = self.monitor.get_agent_health("control") + self.assertEqual(health["status"], HealthStatus.CRITICAL.value) + + def test_unknown_agent_not_registered(self): + health = self.monitor.get_agent_health("nonexistent") + self.assertEqual(health["status"], HealthStatus.UNKNOWN.value) + + def test_compute_overall_healthy(self): + agent_health = { + "a": {"status": "healthy"}, + "b": {"status": "healthy"}, + } + status = self.monitor.compute_overall_status(agent_health) + self.assertEqual(status, HealthStatus.HEALTHY.value) + + def test_compute_overall_degraded(self): + agent_health = { + "a": {"status": "healthy"}, + "b": {"status": "degraded"}, + } + status = self.monitor.compute_overall_status(agent_health) + self.assertEqual(status, HealthStatus.DEGRADED.value) + + def test_compute_overall_critical_overrides_degraded(self): + agent_health = { + "a": {"status": "degraded"}, + "b": {"status": "critical"}, + } + status = self.monitor.compute_overall_status(agent_health) + self.assertEqual(status, HealthStatus.CRITICAL.value) + + def test_generate_health_report_structure(self): + self.monitor.register_agent("test") + report = self.monitor.generate_health_report() + self.assertIn("overall_status", report) + self.assertIn("agents", report) + self.assertIn("alerts", report) + self.assertIn("generated_at", report) + + def test_alert_generated_on_high_cpu(self): + state = self._make_state(name="planning", cpu_usage=90.0) + self.monitor.update_agent_health("planning", state) + alerts = self.monitor.get_active_alerts() + self.assertTrue(any(a["metric"] == "cpu_usage" for a in alerts)) + + def test_alert_generated_on_slow_response(self): + state = self._make_state(name="planning", response_time=3.0) + self.monitor.update_agent_health("planning", state) + alerts = self.monitor.get_active_alerts() + self.assertTrue(any(a["metric"] == "response_time" for a in alerts)) + + def test_predict_failures_empty(self): + predictions = self.monitor.predict_failures() + self.assertEqual(predictions, []) + + def test_predict_failures_high_error_rate(self): + state = self._make_state(name="control", error_count=10, task_count=100) + self.monitor.update_agent_health("control", state) + predictions = self.monitor.predict_failures() + self.assertTrue(any(p["agent"] == "control" for p in predictions)) + + def test_monitor_system_health_multiple_agents(self): + for name in ["perception", "planning", "control"]: + self.monitor.register_agent(name) + system_health = self.monitor.monitor_system_health() + self.assertEqual(len(system_health), 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..72bf419 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,183 @@ +""" +Integration tests for the Resolver Agent with all 5 existing agents. +""" + +import sys +import os +import time +import unittest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from agents.base_agent import AgentState, AgentStatus, BaseAgent +from agents.resolver_agent import ResolverAgent + + +# --------------------------------------------------------------------------- +# Stub agents representing the 5 existing agents +# --------------------------------------------------------------------------- + +class _StubAgent(BaseAgent): + """Generic stub agent for integration tests.""" + + def __init__(self, name: str, priority: int = 0): + super().__init__(name=name, priority=priority) + self._received_messages = [] + + def on_start(self): + pass + + def on_stop(self): + pass + + def execute(self, task): + return {"done": True, "agent": self.name} + + def receive_message(self, message): + self._received_messages.append(message) + super().receive_message(message) + + +class PerceptionAgent(_StubAgent): + def __init__(self): + super().__init__("perception", priority=5) + + +class PlanningAgent(_StubAgent): + def __init__(self): + super().__init__("planning", priority=4) + + +class ControlAgent(_StubAgent): + def __init__(self): + super().__init__("control", priority=3) + + +class CommunicationAgent(_StubAgent): + def __init__(self): + super().__init__("communication", priority=2) + + +class CoordinationAgent(_StubAgent): + def __init__(self): + super().__init__("coordination", priority=1) + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + +class TestResolverIntegration(unittest.TestCase): + def setUp(self): + self.resolver = ResolverAgent() + self.perception = PerceptionAgent() + self.planning = PlanningAgent() + self.control = ControlAgent() + self.communication = CommunicationAgent() + self.coordination = CoordinationAgent() + + self.all_agents = [ + self.perception, self.planning, self.control, + self.communication, self.coordination, + ] + self.resolver.integrate_with_agents(self.all_agents) + + def test_all_agents_registered(self): + for agent in self.all_agents: + self.assertIn(agent.name, self.resolver._monitored_agents) + + def test_resolver_registered_in_all_agents(self): + for agent in self.all_agents: + self.assertIn("resolver", agent._connected_agents) + + def test_monitor_agents_returns_all_statuses(self): + result = self.resolver.monitor_agents() + for agent in self.all_agents: + self.assertIn(agent.name, result) + + def test_conflict_resolution_notifies_agents(self): + conflict = { + "type": "control", + "agents": ["planning", "control"], + "description": "Competing control requests", + } + self.resolver.resolve_conflict(conflict) + # Both planning and control should have received a resolution message + planning_msgs = [ + m for m in self.planning._received_messages + if m.message_type == "conflict_resolution" + ] + control_msgs = [ + m for m in self.control._received_messages + if m.message_type == "conflict_resolution" + ] + self.assertEqual(len(planning_msgs), 1) + self.assertEqual(len(control_msgs), 1) + + def test_health_report_covers_all_agents(self): + for agent in self.all_agents: + agent.start() + health = self.resolver.assess_system_health() + for agent in self.all_agents: + self.assertIn(agent.name, health["agents"]) + for agent in self.all_agents: + agent.stop() + + def test_resource_arbitration_across_agents(self): + requests = [ + {"agent": ag.name, "resource": "GPU"} + for ag in self.all_agents + ] + result = self.resolver.arbitrate_resources(requests) + # perception has highest priority → gets GPU + self.assertEqual(result["allocation"]["GPU"], "perception") + + def test_error_recovery_for_each_agent(self): + for agent in self.all_agents: + error = { + "type": "agent_crash", + "agent": agent.name, + "severity": 3, + } + result = self.resolver.recover_from_error(error) + self.assertIn("recovery_action", result) + + def test_deadlock_breaking_with_agents(self): + # Set up a circular wait + self.resolver.deadlock_detector.update_wait_graph("planning", ["control"]) + self.resolver.deadlock_detector.update_wait_graph("control", ["planning"]) + deadlock = self.resolver.detect_deadlock() + self.assertIsNotNone(deadlock) + result = self.resolver.break_deadlock(deadlock) + self.assertTrue(result["success"]) + + def test_fallback_execution_for_navigation(self): + failure = {"type": "navigation", "depth": 0} + result = self.resolver.execute_fallback(failure) + self.assertIn("executed_steps", result) + self.assertTrue(result["success"]) + + def test_full_task_dispatch(self): + """Verify all execute() task types work end-to-end.""" + tasks = [ + {"type": "health_check"}, + {"type": "detect_deadlock"}, + {"type": "resolve_conflict", + "conflict": {"type": "control", "agents": ["planning", "control"]}}, + {"type": "recover_error", + "error": {"type": "sensor_failure", "sensor": "camera"}}, + {"type": "arbitrate", + "requests": [{"agent": "perception", "resource": "GPU"}]}, + {"type": "execute_fallback", + "failure": {"type": "navigation"}}, + ] + for task in tasks: + with self.subTest(task_type=task["type"]): + result = self.resolver.execute(task) + # detect_deadlock returns None when there is no deadlock (valid) + if task["type"] != "detect_deadlock": + self.assertIsNotNone(result) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_resolver_agent.py b/tests/test_resolver_agent.py new file mode 100644 index 0000000..0f4a613 --- /dev/null +++ b/tests/test_resolver_agent.py @@ -0,0 +1,213 @@ +""" +Tests for the Resolver Agent. +""" + +import sys +import os +import time +import unittest + +# Ensure the repo root is on the path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from agents.base_agent import AgentState, AgentStatus, BaseAgent +from agents.resolver_agent import ResolverAgent + + +# --------------------------------------------------------------------------- +# Minimal concrete agent for tests +# --------------------------------------------------------------------------- + +class _DummyAgent(BaseAgent): + def __init__(self, name: str, priority: int = 0): + super().__init__(name=name, priority=priority) + + def on_start(self): + pass + + def on_stop(self): + pass + + def execute(self, task): + return {"done": True} + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + +class TestResolverAgentInit(unittest.TestCase): + def setUp(self): + self.resolver = ResolverAgent() + + def test_name_and_priority(self): + self.assertEqual(self.resolver.name, "resolver") + self.assertEqual(self.resolver.priority, 6) + + def test_components_initialized(self): + self.assertIsNotNone(self.resolver.conflict_detector) + self.assertIsNotNone(self.resolver.conflict_resolver) + self.assertIsNotNone(self.resolver.error_handler) + self.assertIsNotNone(self.resolver.arbitrator) + self.assertIsNotNone(self.resolver.health_monitor) + self.assertIsNotNone(self.resolver.deadlock_detector) + self.assertIsNotNone(self.resolver.fallback_planner) + + +class TestResolverAgentIntegration(unittest.TestCase): + def setUp(self): + self.resolver = ResolverAgent() + self.perception = _DummyAgent("perception", priority=5) + self.planning = _DummyAgent("planning", priority=4) + self.control = _DummyAgent("control", priority=3) + + def test_integrate_with_agents(self): + agents = [self.perception, self.planning, self.control] + self.resolver.integrate_with_agents(agents) + self.assertIn("perception", self.resolver._monitored_agents) + self.assertIn("planning", self.resolver._monitored_agents) + self.assertIn("control", self.resolver._monitored_agents) + + def test_integrate_connects_resolver_to_agents(self): + self.resolver.integrate_with_agents([self.perception]) + self.assertIn("resolver", self.perception._connected_agents) + + +class TestResolverConflictHandling(unittest.TestCase): + def setUp(self): + self.resolver = ResolverAgent() + self.resolver.integrate_with_agents([ + _DummyAgent("perception"), + _DummyAgent("planning"), + ]) + + def test_resolve_conflict_returns_dict(self): + conflict = { + "type": "control", + "agents": ["perception", "planning"], + "description": "Test conflict", + } + result = self.resolver.resolve_conflict(conflict) + self.assertIsInstance(result, dict) + self.assertIn("winning_agent", result) + + def test_detect_conflicts_returns_list(self): + conflicts = self.resolver.detect_conflicts() + self.assertIsInstance(conflicts, list) + + +class TestResolverErrorRecovery(unittest.TestCase): + def setUp(self): + self.resolver = ResolverAgent() + + def test_recover_from_sensor_error(self): + error = {"type": "sensor_failure", "sensor": "camera", "severity": 2} + result = self.resolver.recover_from_error(error) + self.assertIsInstance(result, dict) + self.assertIn("recovery_action", result) + + def test_recover_from_agent_crash(self): + error = {"type": "agent_crash", "agent": "planning", "severity": 3} + result = self.resolver.recover_from_error(error) + self.assertIn("recovery_action", result) + + +class TestResolverResourceArbitration(unittest.TestCase): + def setUp(self): + self.resolver = ResolverAgent() + + def test_arbitrate_resources_returns_allocation(self): + requests = [ + {"agent": "perception", "resource": "GPU", "priority": 5}, + {"agent": "planning", "resource": "GPU", "priority": 4}, + ] + result = self.resolver.arbitrate_resources(requests) + self.assertIsInstance(result, dict) + allocation = result.get("allocation", {}) + self.assertIn("GPU", allocation) + self.assertEqual(allocation["GPU"], "perception") + + +class TestResolverHealthAssessment(unittest.TestCase): + def setUp(self): + self.resolver = ResolverAgent() + self.resolver.integrate_with_agents([ + _DummyAgent("perception"), + _DummyAgent("planning"), + ]) + # Start agents so they report HEALTHY + for agent in self.resolver._monitored_agents.values(): + agent.start() + + def tearDown(self): + for agent in self.resolver._monitored_agents.values(): + agent.stop() + + def test_assess_system_health_returns_dict(self): + health = self.resolver.assess_system_health() + self.assertIsInstance(health, dict) + self.assertIn("overall_status", health) + self.assertIn("agents", health) + self.assertIn("alerts", health) + + def test_generate_health_report_has_timestamp(self): + report = self.resolver.generate_health_report() + self.assertIn("report_generated_at", report) + + +class TestResolverDeadlockDetection(unittest.TestCase): + def setUp(self): + self.resolver = ResolverAgent() + + def test_detect_deadlock_no_deadlock(self): + result = self.resolver.detect_deadlock() + self.assertIsNone(result) + + def test_break_deadlock(self): + self.resolver.deadlock_detector.update_wait_graph("A", ["B"]) + self.resolver.deadlock_detector.update_wait_graph("B", ["A"]) + deadlock = self.resolver.detect_deadlock() + self.assertIsNotNone(deadlock) + result = self.resolver.break_deadlock(deadlock) + self.assertTrue(result.get("success")) + + +class TestResolverFallbackExecution(unittest.TestCase): + def setUp(self): + self.resolver = ResolverAgent() + + def test_execute_fallback_navigation(self): + failure = {"type": "navigation", "depth": 0} + result = self.resolver.execute_fallback(failure) + self.assertIsInstance(result, dict) + self.assertIn("executed_steps", result) + self.assertTrue(result.get("success")) + + +class TestResolverExecuteDispatch(unittest.TestCase): + def setUp(self): + self.resolver = ResolverAgent() + + def test_execute_health_check(self): + result = self.resolver.execute({"type": "health_check"}) + self.assertIn("overall_status", result) + + def test_execute_unknown_task(self): + result = self.resolver.execute({"type": "nonexistent"}) + self.assertIn("error", result) + + def test_execute_detect_deadlock(self): + result = self.resolver.execute({"type": "detect_deadlock"}) + self.assertIsNone(result) # no deadlock in fresh state + + def test_execute_resolve_conflict(self): + task = { + "type": "resolve_conflict", + "conflict": {"type": "control", "agents": ["planning", "control"]}, + } + result = self.resolver.execute(task) + self.assertIn("strategy", result) + + +if __name__ == "__main__": + unittest.main() From eebebb3fa570694302128f0bda95f750ee1c72e3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 02:39:25 +0000 Subject: [PATCH 3/3] Add .gitignore to exclude pycache and build artifacts Co-authored-by: Stacey77 <54900383+Stacey77@users.noreply.github.com> --- .gitignore | 45 ++++++++++++++++++ agents/__pycache__/__init__.cpython-312.pyc | Bin 528 -> 0 bytes agents/__pycache__/base_agent.cpython-312.pyc | Bin 8514 -> 0 bytes .../resolver_agent.cpython-312.pyc | Bin 14109 -> 0 bytes resolver/__pycache__/__init__.cpython-312.pyc | Bin 1006 -> 0 bytes .../__pycache__/arbitrator.cpython-312.pyc | Bin 9829 -> 0 bytes .../conflict_resolution.cpython-312.pyc | Bin 19103 -> 0 bytes .../deadlock_detector.cpython-312.pyc | Bin 8624 -> 0 bytes .../error_recovery.cpython-312.pyc | Bin 11129 -> 0 bytes .../fallback_planner.cpython-312.pyc | Bin 7650 -> 0 bytes .../health_monitor.cpython-312.pyc | Bin 12487 -> 0 bytes .../__pycache__/predictor.cpython-312.pyc | Bin 10653 -> 0 bytes tests/__pycache__/__init__.cpython-312.pyc | Bin 180 -> 0 bytes ...st_arbitrator.cpython-312-pytest-9.0.2.pyc | Bin 6995 -> 0 bytes ...ct_resolution.cpython-312-pytest-9.0.2.pyc | Bin 12545 -> 0 bytes ...rror_recovery.cpython-312-pytest-9.0.2.pyc | Bin 9168 -> 0 bytes ...ealth_monitor.cpython-312-pytest-9.0.2.pyc | Bin 10181 -> 0 bytes ...t_integration.cpython-312-pytest-9.0.2.pyc | Bin 11188 -> 0 bytes ...esolver_agent.cpython-312-pytest-9.0.2.pyc | Bin 15153 -> 0 bytes 19 files changed, 45 insertions(+) create mode 100644 .gitignore delete mode 100644 agents/__pycache__/__init__.cpython-312.pyc delete mode 100644 agents/__pycache__/base_agent.cpython-312.pyc delete mode 100644 agents/__pycache__/resolver_agent.cpython-312.pyc delete mode 100644 resolver/__pycache__/__init__.cpython-312.pyc delete mode 100644 resolver/__pycache__/arbitrator.cpython-312.pyc delete mode 100644 resolver/__pycache__/conflict_resolution.cpython-312.pyc delete mode 100644 resolver/__pycache__/deadlock_detector.cpython-312.pyc delete mode 100644 resolver/__pycache__/error_recovery.cpython-312.pyc delete mode 100644 resolver/__pycache__/fallback_planner.cpython-312.pyc delete mode 100644 resolver/__pycache__/health_monitor.cpython-312.pyc delete mode 100644 resolver/__pycache__/predictor.cpython-312.pyc delete mode 100644 tests/__pycache__/__init__.cpython-312.pyc delete mode 100644 tests/__pycache__/test_arbitrator.cpython-312-pytest-9.0.2.pyc delete mode 100644 tests/__pycache__/test_conflict_resolution.cpython-312-pytest-9.0.2.pyc delete mode 100644 tests/__pycache__/test_error_recovery.cpython-312-pytest-9.0.2.pyc delete mode 100644 tests/__pycache__/test_health_monitor.cpython-312-pytest-9.0.2.pyc delete mode 100644 tests/__pycache__/test_integration.cpython-312-pytest-9.0.2.pyc delete mode 100644 tests/__pycache__/test_resolver_agent.cpython-312-pytest-9.0.2.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29eda91 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +.Python +*.egg +*.egg-info/ +dist/ +build/ +eggs/ +parts/ +var/ +sdist/ +develop-eggs/ +.installed.cfg +lib/ +lib64/ +*.so + +# Virtual environments +.env +.venv +env/ +venv/ +ENV/ + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +.tox/ + +# IDEs +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Node +node_modules/ diff --git a/agents/__pycache__/__init__.cpython-312.pyc b/agents/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 17a7cc68048e8cb125de42a875e69c78005713de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 528 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 diff --git a/resolver/__pycache__/__init__.cpython-312.pyc b/resolver/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index ba09822a25890b06519c5cb3e201278c9c7baa9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1006 zcmZWnJ#W-N5Z(2C#+M_PToDB!wC#y-h5|)M2q_{YASEbDIu}{_?9H8(UGHhV2|7A{ zMMO)-FQK5KpjqjZf{G4^Xi_n|wl7Fn@_2SU`{vEO`O@wB2%h_&FVhb-gudHD^?0Ro zJq70yB`8D*PCSl<7kb$GHC_|-ur3;5Lo~yti`98cw8OUZ8+=Llp)WdN$DNyeS#-m$ z^IQCmSP560-{w8QSjA|JlBJJ0T$}lW&bx2^5z8gNV2Vtm_$(STaw-+k6GpbjEYoRB zw)b8WB}WqcoaD1yGcol2LnSZLgykfb*(pzBO_T*X*Qv}l2~!GC6pJMw&o&9*W|~B5 zlxh{Z6o0}Zt|vsuEY(t_*?5yAEK0bH&%n_vE|5+m&PTwEOnH=LE-lIu|6u>&D9TxK zTYjoo0yLof(EnklI>2Vd^{`!*m1}F%SE_lqK^Hk3)i`}w^L)!vV5bC~i-5(Uq53p)j!oI4*M>Van0n-Lf}>zG+dK$e2>ocai{)VKsTF+$D2w@u!lH-pW_7uml_x zb9a$k2}BEl0L>OQ>x%@ZJ3tk>-%>RjiI8)gmi9jo#a_^IFI;ZoSLoy`I+>%bE3`XDPv+?H9PRzCcRJqtmRg7W EzY(K3mjD0& diff --git a/resolver/__pycache__/arbitrator.cpython-312.pyc b/resolver/__pycache__/arbitrator.cpython-312.pyc deleted file mode 100644 index 7d66a41faf155d6720a38c07d60c7b9fc5230616..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9829 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 diff --git a/resolver/__pycache__/conflict_resolution.cpython-312.pyc b/resolver/__pycache__/conflict_resolution.cpython-312.pyc deleted file mode 100644 index 1d2406f20a7e5703691a9057f7c2021855234ff5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19103 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

    In#s_$`F9aw|Ny%p<|%PGhlj zsXIs(9K8b5NVyWZEt9+%nIp%L#E~R`D7f^;;OU_096Svjspeg9Si!+%T!-o4|3J;M zx$XY>jHR%*Im13}5$<2j@I}^TZTO3Dk%FHur~W4S&AA72YU|YG!UXpNlwnmVeC7NWFHd5R-JEMc!dN#gt7;K=N9#xv2Z*H zw6SHK^}T>kPDhJ-8Ezn}6e;*w^R9(|-AFDUB{#7L-ffKjfPQY`xf6wkrXu<6I=B*k z@R5Ff>ii?|Uk3guqMmw>bUd>t~-sP>%Qq@b_Z(Zub^CRfg_dRB%X zd{(63cCB+wd30{=#;*gLcw0P4nQMv+!@ZduTJf%gR|i(69=58T$6vrpyt!#IMvLxd z&b$0UR$Li)5K&tWy`b}CtV@cF@rC~V&huJ@@-9-`n+|oYTdhhJZJ5k$AFBnf5*6J$XJ2En=|$z-hS8l N9z%OyP)NxO|1Vqt_NxE@ diff --git a/resolver/__pycache__/error_recovery.cpython-312.pyc b/resolver/__pycache__/error_recovery.cpython-312.pyc deleted file mode 100644 index 5e1030836a69c11bbe11920243ec63a0a4fe56fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11129 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 X_ieD(-Oo^bxqFi5PCp_jnI--kUHgW7 diff --git a/resolver/__pycache__/health_monitor.cpython-312.pyc b/resolver/__pycache__/health_monitor.cpython-312.pyc deleted file mode 100644 index f6c0125111f62f20b5c4449bb8a33842212b7c84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12487 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 Biwgh% diff --git a/resolver/__pycache__/predictor.cpython-312.pyc b/resolver/__pycache__/predictor.cpython-312.pyc deleted file mode 100644 index 3d63370922860ba1b49f947d2e84b8c6be15cd44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10653 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 diff --git a/tests/__pycache__/test_arbitrator.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_arbitrator.cpython-312-pytest-9.0.2.pyc deleted file mode 100644 index f52f9bdc64490866253097ec202edeebf9622c1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6995 zcmd5>T}&L;6}~gOvpc(9SQZTSZj%^O+x3Eh82IJ?GBO{)4fb zM79Ek~G2WR#;!%o&ChDG*ugyr2+W{}WqJLjyqj?TtPE%6BiKC_ z+4hJxYu1y{v zHs*7g^+%Z|j->b$9~5Wzxr35y`m;*B$=Qs?&mlpG|RWN z1bM^Ti~YUXZllJktBLq94pGKaW0^(RCv#Bp;zRhkKok<@05kyW4hh?29;jWm0TpCB zPzN3q0CNBpWhYRVECO}QE}%PPH&Bnf189Zp0a__n0QJh1K&w*TV3kpW=PBCdn6(Mq z5y~4S_M!pE0!e23EZNKvDchDJK`s>JDGo5)Ik5ye!>%dOs~U3Fe}A-fA`w$s>2y4< z(AJp*z1B)2Gof$1Lyr&N-}|6;xi?+a{k3=Ki8%Bh z_-J!x zMkDdK8Xsqx3KB&Np*|pndrARKol1fqqPw9X0}sWCj2$Kr10PLCq7O3)4BBm~D18H1 z3YB>OCUUjRK!TCe%53K=KvFmqwO#86a*I5xtA7yDJ#VkoH7s$ur)^!Z?R3wJ>msSH z%Y&;U?dzX+JVUs4>*DVFb&I0z4?gHx5j#o&Lv3bO@eNcgz-0U9Hd#Q(u=XQ>=0I5| z9Y9!_wC-xF0lt&$X3uMYY7XR0)xpWsfy;4%I*1{<2SzhZ7y-0%s^C&$3Wa-WEM<7c z5(z4+@d)aLA!?Z$%G3}CGhn76v;n6=XJS+joFBM&dLYzySsJ=DaPi_`|9~M}k3^>x z`g7;gjFJD1fN{1i7rO~%#6lPi0J$-9+`coQ z#xqLe*8NRSn|oH9hxO*+)#l53^W~>vIG=;5)rl4Hc&S>Ue3)8&3Y8z**@80Q#a2|n zDf6fdYM$DFGT1^4Qn;d{(Zm=-J7_ok^P4&UY~5#4j*A9hG826a)C#=v!6If3B>Z-RoK96xCL9y`;@j~D>KJZb342ZM8MUn<8e(gZ|T{A*Mz2U znhw~MHV6dGuVG2hJ;&F;Z!5azXsPKj@NT4iXENYrOphU@=>DTi(G{_;6fSg}%)0&p zl~TCCbvxLt3(+xt$L&A(tt&D&TLRyvZrS8#ZeJD(oU%oKuQ5#tK0KnRQDV~Z@XXj zbga7sc0qfx7xc>ke=7Es7%fu;bw%tcRfVjh{b{0FikWXFU@~^){ADttU6ZW9#q^XV zPO><3Ef|w=kJ|}d%ZwXBOo~F+4&B$WT%GoHl{&_HYUDT}g; z#YDE(HYr|qGvnET^rmeSld;Jj#^o$9mWq(UCl51oc#iobWMI51=LIx;vWYQ(38Sd$Z|Hp6JqT;8jDy4Ia`pLls! z-ga9gH7tt#U*cmk>Sb<|DL_5!pB_i@3m_vDZ6#<%wMJdejAzv31j|j88%gtKMgKw@ zku)JGy=aRQp!_Mos59a9wi2N7L(#Emmw}+ag(wy-QuMR$>z>xq3$u=lPg?bAK;W&gcHe4kyI$Kq@6H7sfyJ}BzxAt{rSR&pQ~I$}>0@WohX+^0v!!)T zvPx01xUZ`!lXnd!5@?Jq*0q_S%mF9@DsKmY&$ diff --git a/tests/__pycache__/test_conflict_resolution.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_conflict_resolution.cpython-312-pytest-9.0.2.pyc deleted file mode 100644 index 8665def686e7f26c56a1db78ed15c48d0fe75233..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12545 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 diff --git a/tests/__pycache__/test_error_recovery.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_error_recovery.cpython-312-pytest-9.0.2.pyc deleted file mode 100644 index 187402e40efd4705983ce74f75a0de2c720214dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9168 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 diff --git a/tests/__pycache__/test_integration.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_integration.cpython-312-pytest-9.0.2.pyc deleted file mode 100644 index a4d00077716990c47393b8db3918560712d4195d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11188 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 diff --git a/tests/__pycache__/test_resolver_agent.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_resolver_agent.cpython-312-pytest-9.0.2.pyc deleted file mode 100644 index 3976be8f19948bbb6eabe512e14d6132984a57a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15153 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 AJpcdz