#!/usr/bin/env python3 """ GOAP Executor Module - Allegro-Primus Child Autonomy System Executes action plans and manages plan lifecycle. """ import asyncio import time import json from typing import Dict, List, Optional, Any, Callable from dataclasses import dataclass, field from enum import Enum, auto from pathlib import Path from datetime import datetime from actions import Action, ActionResult, ActionStatus from planner import Plan, PlanStatus, GOAPPlanner from goals import Goal class ExecutionMode(Enum): """Execution modes""" SEQUENTIAL = auto() # Execute actions one by one PARALLEL = auto() # Execute independent actions in parallel ADAPTIVE = auto() # Adapt based on context @dataclass class ExecutionContext: """Context for action execution""" world_state: Dict[str, Any] user_context: Dict[str, Any] = field(default_factory=dict) system_context: Dict[str, Any] = field(default_factory=dict) execution_history: List[Dict] = field(default_factory=list) metadata: Dict[str, Any] = field(default_factory=dict) def copy(self) -> 'ExecutionContext': """Create a copy of the context""" return ExecutionContext( world_state=self.world_state.copy(), user_context=self.user_context.copy(), system_context=self.system_context.copy(), execution_history=self.execution_history.copy(), metadata=self.metadata.copy() ) @dataclass class ExecutionReport: """Report of plan execution""" plan: Plan success: bool actions_completed: int actions_failed: int execution_time: float start_time: float end_time: float final_state: Dict[str, Any] error: Optional[str] = None action_reports: List[Dict] = field(default_factory=list) def to_dict(self) -> Dict: return { 'plan': self.plan.to_dict(), 'success': self.success, 'actions_completed': self.actions_completed, 'actions_failed': self.actions_failed, 'execution_time': self.execution_time, 'start_time': self.start_time, 'end_time': self.end_time, 'final_state': self.final_state, 'error': self.error, 'action_reports': self.action_reports } class PlanExecutor: """Executes plans and manages their lifecycle""" def __init__( self, mode: ExecutionMode = ExecutionMode.SEQUENTIAL, max_retries: int = 2, retry_delay: float = 1.0, state_persistence_path: Optional[str] = None ): self.mode = mode self.max_retries = max_retries self.retry_delay = retry_delay self.state_persistence_path = state_persistence_path or "/root/allegro/goap/executor_state.json" # Execution tracking self.current_plan: Optional[Plan] = None self.current_action_index: int = 0 self.is_running: bool = False self.should_cancel: bool = False # Statistics self.total_plans_executed = 0 self.total_plans_succeeded = 0 self.total_actions_executed = 0 self.total_actions_succeeded = 0 # Callbacks self.on_action_start: Optional[Callable[[Action, int], None]] = None self.on_action_complete: Optional[Callable[[Action, ActionResult, int], None]] = None self.on_plan_complete: Optional[Callable[[ExecutionReport], None]] = None # Load persisted state self._load_state() def _load_state(self): """Load persisted executor state""" import os if os.path.exists(self.state_persistence_path): try: with open(self.state_persistence_path, 'r') as f: data = json.load(f) self.total_plans_executed = data.get('total_plans_executed', 0) self.total_plans_succeeded = data.get('total_plans_succeeded', 0) self.total_actions_executed = data.get('total_actions_executed', 0) self.total_actions_succeeded = data.get('total_actions_succeeded', 0) except Exception as e: print(f"[Executor] Failed to load state: {e}") def _save_state(self): """Persist executor state""" try: Path(self.state_persistence_path).parent.mkdir(parents=True, exist_ok=True) with open(self.state_persistence_path, 'w') as f: json.dump({ 'total_plans_executed': self.total_plans_executed, 'total_plans_succeeded': self.total_plans_succeeded, 'total_actions_executed': self.total_actions_executed, 'total_actions_succeeded': self.total_actions_succeeded, 'last_updated': time.time() }, f, indent=2) except Exception as e: print(f"[Executor] Failed to save state: {e}") async def execute_plan( self, plan: Plan, initial_context: ExecutionContext, replan_on_failure: bool = True, planner: Optional[GOAPPlanner] = None ) -> ExecutionReport: """ Execute a plan with the given context. Args: plan: The plan to execute initial_context: Initial execution context replan_on_failure: Whether to replan if actions fail planner: Planner to use for replanning Returns: ExecutionReport with results """ self.current_plan = plan self.current_action_index = 0 self.is_running = True self.should_cancel = False start_time = time.time() plan.status = PlanStatus.IN_PROGRESS plan.started_at = start_time context = initial_context.copy() action_reports = [] actions_completed = 0 actions_failed = 0 try: # Execute actions based on mode if self.mode == ExecutionMode.SEQUENTIAL: result = await self._execute_sequential( plan, context, replan_on_failure, planner ) elif self.mode == ExecutionMode.PARALLEL: result = await self._execute_parallel(plan, context) else: # ADAPTIVE result = await self._execute_adaptive( plan, context, replan_on_failure, planner ) # Update plan status if result['success']: plan.status = PlanStatus.COMPLETED self.total_plans_succeeded += 1 else: plan.status = PlanStatus.FAILED self.total_plans_executed += 1 end_time = time.time() plan.completed_at = end_time report = ExecutionReport( plan=plan, success=result['success'], actions_completed=result['completed'], actions_failed=result['failed'], execution_time=end_time - start_time, start_time=start_time, end_time=end_time, final_state=context.world_state, error=result.get('error'), action_reports=result.get('reports', []) ) # Trigger callback if self.on_plan_complete: self.on_plan_complete(report) self._save_state() return report except Exception as e: plan.status = PlanStatus.FAILED end_time = time.time() plan.completed_at = end_time return ExecutionReport( plan=plan, success=False, actions_completed=actions_completed, actions_failed=actions_failed + 1, execution_time=end_time - start_time, start_time=start_time, end_time=end_time, final_state=context.world_state, error=str(e), action_reports=action_reports ) finally: self.is_running = False self.current_plan = None async def _execute_sequential( self, plan: Plan, context: ExecutionContext, replan_on_failure: bool, planner: Optional[GOAPPlanner] ) -> Dict: """Execute actions sequentially""" completed = 0 failed = 0 reports = [] for i, action in enumerate(plan.actions): if self.should_cancel: return { 'success': False, 'completed': completed, 'failed': failed, 'error': 'Execution cancelled', 'reports': reports } self.current_action_index = i # Trigger start callback if self.on_action_start: self.on_action_start(action, i) # Execute action with retries result = await self._execute_action_with_retry(action, context) # Update context context.execution_history.append({ 'action': action.name, 'result': result.success, 'time': time.time() }) # Trigger complete callback if self.on_action_complete: self.on_action_complete(action, result, i) reports.append({ 'action': action.name, 'success': result.success, 'message': result.message, 'execution_time': result.execution_time }) if result.success: completed += 1 # Update world state with effects context.world_state = action.apply_effects(context.world_state) self.total_actions_succeeded += 1 else: failed += 1 # Try to replan if enabled if replan_on_failure and planner: new_plan = planner.replan(plan, context.world_state, i) if new_plan and new_plan.actions: # Replace remaining actions plan.actions = plan.actions[:i+1] + new_plan.actions # Continue with next action continue else: # Replanning failed return { 'success': False, 'completed': completed, 'failed': failed, 'error': f"Action {action.name} failed and replanning unsuccessful", 'reports': reports } else: # No replanning, fail the plan return { 'success': False, 'completed': completed, 'failed': failed, 'error': f"Action {action.name} failed: {result.error}", 'reports': reports } self.total_actions_executed += 1 return { 'success': failed == 0, 'completed': completed, 'failed': failed, 'reports': reports } async def _execute_parallel( self, plan: Plan, context: ExecutionContext ) -> Dict: """Execute independent actions in parallel""" # For now, execute sequentially (parallel execution is complex) # TODO: Implement proper parallel execution with dependency tracking return await self._execute_sequential(plan, context, False, None) async def _execute_adaptive( self, plan: Plan, context: ExecutionContext, replan_on_failure: bool, planner: Optional[GOAPPlanner] ) -> Dict: """ Execute adaptively based on context. Adjusts execution strategy based on system load, urgency, etc. """ # Check system load cpu_percent = context.world_state.get('system', {}).get('cpu_percent', 50) if cpu_percent > 80: # High load - slow down and prioritize await asyncio.sleep(1) # Check urgency urgency = plan.goal.state.urgency if plan.goal else 0 if urgency > 0.8: # High urgency - skip non-essential actions essential_actions = [ a for a in plan.actions if a.category in ['system', 'safety'] ] plan.actions = essential_actions return await self._execute_sequential(plan, context, replan_on_failure, planner) async def _execute_action_with_retry( self, action: Action, context: ExecutionContext ) -> ActionResult: """Execute an action with retry logic""" last_result = None for attempt in range(self.max_retries + 1): try: # Prepare action context action_context = { 'world_state': context.world_state, 'user_context': context.user_context, 'system_context': context.system_context, 'attempt': attempt, **context.metadata } # Execute action result = await action.execute(action_context) if result.success: action.success_count += 1 return result last_result = result # Retry if not last attempt if attempt < self.max_retries: await asyncio.sleep(self.retry_delay * (attempt + 1)) except Exception as e: last_result = ActionResult.failure_result( error=str(e), message=f"Exception during action execution: {action.name}" ) if attempt < self.max_retries: await asyncio.sleep(self.retry_delay * (attempt + 1)) action.execution_count += 1 return last_result or ActionResult.failure_result( error="All retry attempts failed" ) def cancel(self): """Cancel current execution""" self.should_cancel = True def get_status(self) -> Dict: """Get current execution status""" if not self.is_running or not self.current_plan: return { 'running': False, 'plan': None, 'progress': 0 } total = len(self.current_plan.actions) progress = (self.current_action_index / total * 100) if total > 0 else 0 return { 'running': True, 'plan': self.current_plan.to_dict(), 'current_action': self.current_action_index, 'total_actions': total, 'progress': progress, 'current_action_name': ( self.current_plan.actions[self.current_action_index].name if self.current_action_index < total else None ) } def get_stats(self) -> Dict: """Get execution statistics""" return { 'total_plans_executed': self.total_plans_executed, 'total_plans_succeeded': self.total_plans_succeeded, 'plan_success_rate': self.total_plans_succeeded / max(1, self.total_plans_executed), 'total_actions_executed': self.total_actions_executed, 'total_actions_succeeded': self.total_actions_succeeded, 'action_success_rate': self.total_actions_succeeded / max(1, self.total_actions_executed), 'mode': self.mode.name } class ExecutionScheduler: """Schedules plan execution based on priorities and constraints""" def __init__(self, executor: PlanExecutor): self.executor = executor self.queue: List[Tuple[float, Plan, ExecutionContext]] = [] self.running: bool = False self._task: Optional[asyncio.Task] = None def schedule( self, plan: Plan, context: ExecutionContext, priority: float = 1.0 ): """Schedule a plan for execution""" # Adjust priority by goal urgency if plan.goal: priority += plan.goal.state.urgency * 10 priority += plan.goal.state.effective_priority / 100 self.queue.append((priority, plan, context)) self.queue.sort(key=lambda x: -x[0]) # Highest priority first async def start(self): """Start the scheduler""" self.running = True self._task = asyncio.create_task(self._run_loop()) async def stop(self): """Stop the scheduler""" self.running = False if self._task: self._task.cancel() try: await self._task except asyncio.CancelledError: pass async def _run_loop(self): """Main scheduler loop""" while self.running: if self.queue and not self.executor.is_running: priority, plan, context = self.queue.pop(0) # Execute plan try: report = await self.executor.execute_plan(plan, context) # Log completion print(f"[Scheduler] Plan completed: {plan.goal.name if plan.goal else 'unknown'}") print(f" Success: {report.success}") print(f" Actions: {report.actions_completed}/{report.actions_completed + report.actions_failed}") except Exception as e: print(f"[Scheduler] Plan execution error: {e}") await asyncio.sleep(0.1) def get_queue_status(self) -> Dict: """Get current queue status""" return { 'queued_plans': len(self.queue), 'queue': [ { 'priority': p, 'goal': plan.goal.name if plan.goal else 'unknown', 'actions': len(plan.actions) } for p, plan, _ in self.queue ] } class ExecutionMonitor: """Monitors plan execution and system health""" def __init__(self, executor: PlanExecutor): self.executor = executor self.reports: List[ExecutionReport] = [] self.max_reports = 100 # Set up callbacks executor.on_action_start = self._on_action_start executor.on_action_complete = self._on_action_complete executor.on_plan_complete = self._on_plan_complete def _on_action_start(self, action: Action, index: int): """Called when an action starts""" print(f"[Monitor] Action {index + 1}: {action.name} - STARTED") def _on_action_complete(self, action: Action, result: ActionResult, index: int): """Called when an action completes""" status = "SUCCESS" if result.success else "FAILED" print(f"[Monitor] Action {index + 1}: {action.name} - {status}") if not result.success: print(f" Error: {result.error}") def _on_plan_complete(self, report: ExecutionReport): """Called when a plan completes""" self.reports.append(report) if len(self.reports) > self.max_reports: self.reports.pop(0) # Log summary print(f"[Monitor] Plan execution complete") print(f" Goal: {report.plan.goal.name if report.plan.goal else 'unknown'}") print(f" Success: {report.success}") print(f" Time: {report.execution_time:.2f}s") print(f" Actions: {report.actions_completed} completed, {report.actions_failed} failed") def get_recent_reports(self, count: int = 10) -> List[ExecutionReport]: """Get recent execution reports""" return self.reports[-count:] def get_summary(self) -> Dict: """Get execution summary""" if not self.reports: return {'total_reports': 0} total_success = sum(1 for r in self.reports if r.success) total_time = sum(r.execution_time for r in self.reports) total_actions = sum(r.actions_completed for r in self.reports) return { 'total_reports': len(self.reports), 'success_rate': total_success / len(self.reports), 'average_execution_time': total_time / len(self.reports), 'total_actions_executed': total_actions, 'recent_failures': [ { 'goal': r.plan.goal.name if r.plan.goal else 'unknown', 'error': r.error, 'time': r.end_time } for r in self.reports[-10:] if not r.success ] } # Singleton executor executor = PlanExecutor() if __name__ == "__main__": # Test the executor print("=== GOAP Executor Module Test ===") from goals import SystemHealthGoal from actions import CheckSystemHealth, CleanupResources from planner import GOAPPlanner async def test_execution(): # Create test components goal = SystemHealthGoal() world_state = { 'system': { 'cpu_percent': 45, 'memory_percent': 60, 'disk_percent': 85, 'uptime_hours': 48, 'recent_errors': 2, 'health_checked': False } } # Create plan planner = GOAPPlanner() plan = planner.plan(goal, world_state) if not plan: print("No plan found") return print(f"\nPlan created: {len(plan.actions)} actions") # Create executor and monitor exec = PlanExecutor() monitor = ExecutionMonitor(exec) # Create context context = ExecutionContext( world_state=world_state, user_context={'user_id': 'test_user'}, system_context={'test_mode': True} ) # Execute plan print("\n=== Executing Plan ===") report = await exec.execute_plan(plan, context, replan_on_failure=False) print("\n=== Execution Report ===") print(f"Success: {report.success}") print(f"Actions completed: {report.actions_completed}") print(f"Actions failed: {report.actions_failed}") print(f"Execution time: {report.execution_time:.2f}s") print("\n=== Executor Stats ===") print(json.dumps(exec.get_stats(), indent=2)) # Run test asyncio.run(test_execution())