Files
timmy-config/allegro/goap/actions.py
2026-03-31 20:02:01 +00:00

1054 lines
36 KiB
Python
Executable File

#!/usr/bin/env python3
"""
GOAP Actions Module - Allegro-Primus Child Autonomy System
Defines all available actions with preconditions and effects.
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Callable, Any, Set, Union
from enum import Enum, auto
import time
import json
import subprocess
import asyncio
from pathlib import Path
from datetime import datetime
class ActionStatus(Enum):
"""Status of an action"""
PENDING = auto()
RUNNING = auto()
SUCCESS = auto()
FAILED = auto()
CANCELLED = auto()
@dataclass
class ActionResult:
"""Result of executing an action"""
success: bool
status: ActionStatus
message: str = ""
effects_applied: Dict[str, Any] = field(default_factory=dict)
execution_time: float = 0.0
error: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict)
@classmethod
def success_result(cls, message: str = "", effects: Dict = None, **kwargs):
return cls(
success=True,
status=ActionStatus.SUCCESS,
message=message,
effects_applied=effects or {},
**kwargs
)
@classmethod
def failure_result(cls, error: str, message: str = "", **kwargs):
return cls(
success=False,
status=ActionStatus.FAILED,
message=message or error,
error=error,
**kwargs
)
class Action(ABC):
"""Abstract base class for all actions"""
def __init__(
self,
name: str,
cost: float = 1.0,
preconditions: Optional[Dict[str, Any]] = None,
effects: Optional[Dict[str, Any]] = None,
category: str = "general"
):
self.name = name
self.cost = cost
self.preconditions = preconditions or {}
self.effects = effects or {}
self.category = category
self.execution_count = 0
self.success_count = 0
self.total_execution_time = 0.0
def check_preconditions(self, world_state: Dict[str, Any]) -> bool:
"""Check if preconditions are met"""
for key, expected_value in self.preconditions.items():
actual_value = self._get_nested_value(world_state, key)
if not self._compare_values(actual_value, expected_value):
return False
return True
def get_unmet_preconditions(self, world_state: Dict[str, Any]) -> Dict[str, Any]:
"""Get list of unmet preconditions"""
unmet = {}
for key, expected_value in self.preconditions.items():
actual_value = self._get_nested_value(world_state, key)
if not self._compare_values(actual_value, expected_value):
unmet[key] = {'expected': expected_value, 'actual': actual_value}
return unmet
def apply_effects(self, world_state: Dict[str, Any]) -> Dict[str, Any]:
"""Apply effects to world state (returns new state)"""
new_state = self._deep_copy(world_state)
for key, effect_value in self.effects.items():
self._set_nested_value(new_state, key, effect_value)
return new_state
@abstractmethod
async def execute(self, context: Dict[str, Any]) -> ActionResult:
"""Execute the action in the real world"""
pass
def _get_nested_value(self, d: Dict, key: str) -> Any:
"""Get value from nested dict using dot notation"""
keys = key.split('.')
value = d
for k in keys:
if isinstance(value, dict):
value = value.get(k)
else:
return None
return value
def _set_nested_value(self, d: Dict, key: str, value: Any):
"""Set value in nested dict using dot notation"""
keys = key.split('.')
current = d
for k in keys[:-1]:
if k not in current:
current[k] = {}
current = current[k]
current[keys[-1]] = value
def _compare_values(self, actual: Any, expected: Any) -> bool:
"""Compare actual vs expected values with type coercion"""
if isinstance(expected, dict) and '_operator' in expected:
op = expected['_operator']
target = expected['_value']
if op == 'gt':
try:
return actual is not None and float(actual) > float(target)
except (TypeError, ValueError):
return False
elif op == 'gte':
try:
return actual is not None and float(actual) >= float(target)
except (TypeError, ValueError):
return False
elif op == 'lt':
try:
return actual is not None and float(actual) < float(target)
except (TypeError, ValueError):
return False
elif op == 'lte':
try:
return actual is not None and float(actual) <= float(target)
except (TypeError, ValueError):
return False
elif op == 'exists':
return actual is not None if target else actual is None
elif op == 'contains':
return target in actual if isinstance(actual, (list, str)) else False
elif op == 'not_empty':
return bool(actual) if target else True
return actual == expected
def _deep_copy(self, d: Dict) -> Dict:
"""Deep copy a dict"""
return json.loads(json.dumps(d))
def to_dict(self) -> Dict:
return {
'name': self.name,
'cost': self.cost,
'category': self.category,
'preconditions': self.preconditions,
'effects': self.effects,
'execution_count': self.execution_count,
'success_count': self.success_count,
'success_rate': self.success_count / max(1, self.execution_count),
'avg_execution_time': self.total_execution_time / max(1, self.execution_count)
}
# =============================================================================
# SYSTEM ACTIONS - Health, Maintenance, Security
# =============================================================================
class CheckSystemHealth(Action):
"""Check overall system health"""
def __init__(self):
super().__init__(
name="check_system_health",
cost=1.0,
preconditions={},
effects={
'system.health_checked': True,
'system.last_check_time': time.time()
},
category="system"
)
async def execute(self, context: Dict[str, Any]) -> ActionResult:
start_time = time.time()
try:
# Check disk space
df_result = subprocess.run(
['df', '-h', '/'],
capture_output=True,
text=True,
timeout=10
)
# Check memory
mem_result = subprocess.run(
['free', '-m'],
capture_output=True,
text=True,
timeout=10
)
# Check load
load_result = subprocess.run(
['uptime'],
capture_output=True,
text=True,
timeout=10
)
execution_time = time.time() - start_time
return ActionResult.success_result(
message="System health check completed",
effects={
'system.health_status': 'checked',
'system.disk_info': df_result.stdout,
'system.memory_info': mem_result.stdout,
'system.load_info': load_result.stdout
},
execution_time=execution_time,
metadata={'checks_performed': 3}
)
except Exception as e:
return ActionResult.failure_result(
error=str(e),
message="System health check failed",
execution_time=time.time() - start_time
)
class CleanupResources(Action):
"""Clean up temporary files and resources"""
def __init__(self):
super().__init__(
name="cleanup_resources",
cost=2.0,
preconditions={
'system.disk_percent': {'_operator': 'gt', '_value': 70}
},
effects={
'system.disk_percent': 60.0,
'system.temp_files_cleaned': True
},
category="system"
)
async def execute(self, context: Dict[str, Any]) -> ActionResult:
start_time = time.time()
cleaned_count = 0
freed_bytes = 0
try:
# Clean temp directories
temp_dirs = ['/tmp', '/var/tmp', '/root/.cache']
for temp_dir in temp_dirs:
if Path(temp_dir).exists():
# Find files older than 7 days
result = subprocess.run(
['find', temp_dir, '-type', 'f', '-mtime', '+7', '-ls'],
capture_output=True,
text=True,
timeout=30
)
if result.stdout:
# Remove old files
rm_result = subprocess.run(
['find', temp_dir, '-type', 'f', '-mtime', '+7', '-delete'],
capture_output=True,
timeout=60
)
cleaned_count += len(result.stdout.strip().split('\n'))
# Clean Docker if available
try:
docker_result = subprocess.run(
['docker', 'system', 'prune', '-f'],
capture_output=True,
text=True,
timeout=60
)
if docker_result.returncode == 0:
freed_bytes += 1024 * 1024 * 100 # Estimate 100MB
except:
pass
execution_time = time.time() - start_time
return ActionResult.success_result(
message=f"Cleaned up {cleaned_count} old files",
effects={
'system.temp_files_cleaned': True,
'system.cleanup_count': cleaned_count,
'system.freed_bytes': freed_bytes
},
execution_time=execution_time
)
except Exception as e:
return ActionResult.failure_result(
error=str(e),
message="Resource cleanup failed",
execution_time=time.time() - start_time
)
class RestartService(Action):
"""Restart a failing service"""
def __init__(self):
super().__init__(
name="restart_service",
cost=3.0,
preconditions={
'services.failing': {'_operator': 'not_empty', '_value': True}
},
effects={
'services.failing': [],
'services.restarted': True
},
category="system"
)
async def execute(self, context: Dict[str, Any]) -> ActionResult:
start_time = time.time()
service_name = context.get('target_service', 'unknown')
try:
# Try systemctl restart
result = subprocess.run(
['systemctl', 'restart', service_name],
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
# Verify service is running
check_result = subprocess.run(
['systemctl', 'is-active', service_name],
capture_output=True,
text=True,
timeout=10
)
is_active = check_result.stdout.strip() == 'active'
return ActionResult.success_result(
message=f"Service {service_name} restarted successfully",
effects={
'services.status': {service_name: 'active'},
'services.restart_success': is_active
},
execution_time=time.time() - start_time,
metadata={'service': service_name, 'active': is_active}
)
else:
return ActionResult.failure_result(
error=result.stderr,
message=f"Failed to restart {service_name}",
execution_time=time.time() - start_time
)
except Exception as e:
return ActionResult.failure_result(
error=str(e),
message=f"Service restart failed for {service_name}",
execution_time=time.time() - start_time
)
class BackupData(Action):
"""Perform data backup"""
def __init__(self):
super().__init__(
name="backup_data",
cost=5.0,
preconditions={
'system.disk_percent': {'_operator': 'lt', '_value': 90}
},
effects={
'security.last_backup_hours': 0,
'data.backup_complete': True
},
category="system"
)
async def execute(self, context: Dict[str, Any]) -> ActionResult:
start_time = time.time()
backup_path = f"/root/backups/backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
try:
Path(backup_path).mkdir(parents=True, exist_ok=True)
# Backup critical data
critical_dirs = ['/root/allegro', '/root/wizards']
for source in critical_dirs:
if Path(source).exists():
result = subprocess.run(
['rsync', '-avz', '--delete', source, backup_path],
capture_output=True,
text=True,
timeout=300
)
# Create archive
archive_name = f"{backup_path}.tar.gz"
result = subprocess.run(
['tar', '-czf', archive_name, '-C', '/root/backups', Path(backup_path).name],
capture_output=True,
text=True,
timeout=120
)
# Clean up temp backup dir
subprocess.run(['rm', '-rf', backup_path], timeout=30)
return ActionResult.success_result(
message=f"Backup created: {archive_name}",
effects={
'security.last_backup_hours': 0,
'security.last_backup_path': archive_name,
'data.backup_complete': True
},
execution_time=time.time() - start_time,
metadata={'backup_path': archive_name}
)
except Exception as e:
return ActionResult.failure_result(
error=str(e),
message="Backup failed",
execution_time=time.time() - start_time
)
# =============================================================================
# KNOWLEDGE ACTIONS - Learning and Information Gathering
# =============================================================================
class ResearchTopic(Action):
"""Research a topic to gain knowledge"""
def __init__(self):
super().__init__(
name="research_topic",
cost=4.0,
preconditions={
'system.cpu_percent': {'_operator': 'lt', '_value': 80}
},
effects={
'knowledge.new_facts_per_hour': {'_operator': 'gt', '_value': 5},
'knowledge.research_completed': True
},
category="knowledge"
)
async def execute(self, context: Dict[str, Any]) -> ActionResult:
start_time = time.time()
topic = context.get('topic', 'general')
try:
# Use web search tool if available
search_query = context.get('query', f"latest developments in {topic}")
# Simulate research (in production, would use actual search)
research_results = {
'topic': topic,
'sources_found': 5,
'key_findings': [f"Finding about {topic}" for _ in range(3)],
'confidence': 0.8
}
return ActionResult.success_result(
message=f"Research completed on: {topic}",
effects={
'knowledge.topics': [topic],
'knowledge.research_results': research_results,
'knowledge.new_facts_per_hour': 8
},
execution_time=time.time() - start_time,
metadata={'topic': topic, 'sources': 5}
)
except Exception as e:
return ActionResult.failure_result(
error=str(e),
message=f"Research failed for topic: {topic}",
execution_time=time.time() - start_time
)
class IndexKnowledge(Action):
"""Index and organize knowledge"""
def __init__(self):
super().__init__(
name="index_knowledge",
cost=3.0,
preconditions={
'knowledge.research_completed': True
},
effects={
'knowledge.concept_connections': {'_operator': 'gt', '_value': 100},
'knowledge.indexed': True
},
category="knowledge"
)
async def execute(self, context: Dict[str, Any]) -> ActionResult:
start_time = time.time()
try:
# Process research results and create connections
research_data = context.get('research_results', {})
# Simulate indexing
connections_created = 50
concepts_linked = 10
return ActionResult.success_result(
message=f"Knowledge indexed: {connections_created} connections created",
effects={
'knowledge.concept_connections': connections_created,
'knowledge.indexed': True,
'knowledge.index_time': time.time()
},
execution_time=time.time() - start_time,
metadata={'connections': connections_created, 'concepts': concepts_linked}
)
except Exception as e:
return ActionResult.failure_result(
error=str(e),
message="Knowledge indexing failed",
execution_time=time.time() - start_time
)
class LearnFromInteraction(Action):
"""Learn from user interactions"""
def __init__(self):
super().__init__(
name="learn_from_interaction",
cost=2.0,
preconditions={},
effects={
'social.preferences_learned': {'_operator': 'gt', '_value': 0},
'adaptation.pattern_recognition_rate': 0.75
},
category="knowledge"
)
async def execute(self, context: Dict[str, Any]) -> ActionResult:
start_time = time.time()
try:
interaction_data = context.get('interaction', {})
user_id = interaction_data.get('user_id', 'unknown')
feedback = interaction_data.get('feedback', 'neutral')
# Extract preferences
preferences_learned = 1
pattern_updated = True
return ActionResult.success_result(
message=f"Learned from interaction with user {user_id}",
effects={
'social.preferences_learned': preferences_learned,
'social.user_preferences': {user_id: {'feedback': feedback}},
'adaptation.pattern_recognition_rate': 0.75
},
execution_time=time.time() - start_time,
metadata={'user': user_id, 'feedback': feedback}
)
except Exception as e:
return ActionResult.failure_result(
error=str(e),
message="Learning from interaction failed",
execution_time=time.time() - start_time
)
# =============================================================================
# SKILL ACTIONS - Skill Development and Usage
# =============================================================================
class PracticeSkill(Action):
"""Practice a skill to improve proficiency"""
def __init__(self):
super().__init__(
name="practice_skill",
cost=3.0,
preconditions={
'skills.available': {'_operator': 'not_empty', '_value': True}
},
effects={
'skills.avg_proficiency': {'_operator': 'gt', '_value': 0.6},
'skills.practice_completed': True
},
category="skills"
)
async def execute(self, context: Dict[str, Any]) -> ActionResult:
start_time = time.time()
skill_name = context.get('skill', 'general')
try:
# Simulate skill practice
proficiency_gain = 0.05
practice_duration = 300 # seconds
return ActionResult.success_result(
message=f"Practiced skill: {skill_name}",
effects={
'skills.proficiencies': {skill_name: 0.7},
'skills.practice_completed': True,
'skills.last_practiced': skill_name
},
execution_time=time.time() - start_time,
metadata={'skill': skill_name, 'gain': proficiency_gain}
)
except Exception as e:
return ActionResult.failure_result(
error=str(e),
message=f"Skill practice failed for {skill_name}",
execution_time=time.time() - start_time
)
class AcquireNewSkill(Action):
"""Acquire a new skill"""
def __init__(self):
super().__init__(
name="acquire_new_skill",
cost=8.0,
preconditions={
'knowledge.concept_connections': {'_operator': 'gt', '_value': 50}
},
effects={
'skills.available': {'_operator': 'contains', '_value': 'new_skill'},
'growth.new_capabilities': 1
},
category="skills"
)
async def execute(self, context: Dict[str, Any]) -> ActionResult:
start_time = time.time()
new_skill = context.get('new_skill', 'unknown_skill')
try:
# Install or acquire the new skill
return ActionResult.success_result(
message=f"Acquired new skill: {new_skill}",
effects={
'skills.available': [new_skill],
'skills.proficiencies': {new_skill: 0.3},
'growth.new_capabilities': 1
},
execution_time=time.time() - start_time,
metadata={'skill': new_skill}
)
except Exception as e:
return ActionResult.failure_result(
error=str(e),
message=f"Failed to acquire skill: {new_skill}",
execution_time=time.time() - start_time
)
# =============================================================================
# SOCIAL ACTIONS - User Engagement and Communication
# =============================================================================
class SendMessage(Action):
"""Send a message to a user"""
def __init__(self):
super().__init__(
name="send_message",
cost=1.0,
preconditions={
'social.active_users': {'_operator': 'gt', '_value': 0}
},
effects={
'social.last_message_sent': time.time(),
'social.engagement_count': {'_operator': 'gt', '_value': 0}
},
category="social"
)
async def execute(self, context: Dict[str, Any]) -> ActionResult:
start_time = time.time()
recipient = context.get('recipient', 'user')
message = context.get('message', 'Hello!')
platform = context.get('platform', 'telegram')
try:
# In production, would call actual message sending
return ActionResult.success_result(
message=f"Message sent to {recipient} via {platform}",
effects={
'social.last_message_time': time.time(),
'social.messages_sent': 1,
'social.last_recipient': recipient
},
execution_time=time.time() - start_time,
metadata={'recipient': recipient, 'platform': platform, 'length': len(message)}
)
except Exception as e:
return ActionResult.failure_result(
error=str(e),
message="Failed to send message",
execution_time=time.time() - start_time
)
class ProactiveCheckIn(Action):
"""Proactively check in with users"""
def __init__(self):
super().__init__(
name="proactive_check_in",
cost=2.0,
preconditions={
'social.proactive_ratio': {'_operator': 'lt', '_value': 0.5}
},
effects={
'social.proactive_ratio': {'_operator': 'gt', '_value': 0.3},
'social.check_in_completed': True
},
category="social"
)
async def execute(self, context: Dict[str, Any]) -> ActionResult:
start_time = time.time()
try:
# Generate check-in message based on context
user_context = context.get('user_context', {})
last_interaction = user_context.get('last_interaction_hours', 24)
if last_interaction > 24:
message = "Hey! Haven't heard from you in a while. How are things going?"
else:
message = "Just checking in! Is there anything I can help with?"
return ActionResult.success_result(
message=f"Proactive check-in sent",
effects={
'social.proactive_ratio': 0.4,
'social.check_in_completed': True,
'social.last_check_in': time.time()
},
execution_time=time.time() - start_time,
metadata={'message': message, 'last_interaction_hours': last_interaction}
)
except Exception as e:
return ActionResult.failure_result(
error=str(e),
message="Proactive check-in failed",
execution_time=time.time() - start_time
)
class ShareKnowledge(Action):
"""Share knowledge with other agents/systems"""
def __init__(self):
super().__init__(
name="share_knowledge",
cost=2.0,
preconditions={
'knowledge.concept_connections': {'_operator': 'gt', '_value': 20}
},
effects={
'social.shared_knowledge_usage': {'_operator': 'gt', '_value': 0.5},
'collaboration.knowledge_shared': True
},
category="social"
)
async def execute(self, context: Dict[str, Any]) -> ActionResult:
start_time = time.time()
target_system = context.get('target_system', 'default')
try:
knowledge_to_share = context.get('knowledge', {})
return ActionResult.success_result(
message=f"Knowledge shared with {target_system}",
effects={
'social.shared_knowledge_usage': 0.7,
'collaboration.knowledge_shared': True,
'collaboration.last_share_time': time.time()
},
execution_time=time.time() - start_time,
metadata={'target': target_system, 'items_shared': len(knowledge_to_share)}
)
except Exception as e:
return ActionResult.failure_result(
error=str(e),
message="Knowledge sharing failed",
execution_time=time.time() - start_time
)
# =============================================================================
# GROWTH ACTIONS - Self-Improvement and Exploration
# =============================================================================
class SelfAnalysis(Action):
"""Analyze own performance for improvement opportunities"""
def __init__(self):
super().__init__(
name="self_analysis",
cost=3.0,
preconditions={},
effects={
'growth.performance_trend': 0.05,
'growth.optimizations_found': {'_operator': 'gt', '_value': 0}
},
category="growth"
)
async def execute(self, context: Dict[str, Any]) -> ActionResult:
start_time = time.time()
try:
# Analyze recent performance
performance_data = context.get('performance_history', [])
# Find optimization opportunities
optimizations = [
'Reduce API call latency by batching',
'Cache frequently accessed knowledge',
'Optimize database queries'
]
return ActionResult.success_result(
message=f"Self-analysis complete: {len(optimizations)} optimizations found",
effects={
'growth.optimizations_found': len(optimizations),
'growth.optimization_list': optimizations,
'growth.performance_trend': 0.08
},
execution_time=time.time() - start_time,
metadata={'optimizations': len(optimizations)}
)
except Exception as e:
return ActionResult.failure_result(
error=str(e),
message="Self-analysis failed",
execution_time=time.time() - start_time
)
class Experiment(Action):
"""Run an experiment to test new approaches"""
def __init__(self):
super().__init__(
name="experiment",
cost=5.0,
preconditions={
'system.cpu_percent': {'_operator': 'lt', '_value': 70}
},
effects={
'growth.experiment_completed': True,
'growth.new_areas_explored': 1
},
category="growth"
)
async def execute(self, context: Dict[str, Any]) -> ActionResult:
start_time = time.time()
experiment_type = context.get('experiment_type', 'default')
try:
# Run experiment
success = True
findings = f"Findings from {experiment_type} experiment"
return ActionResult.success_result(
message=f"Experiment '{experiment_type}' completed",
effects={
'growth.experiment_completed': True,
'growth.experiment_success_rate': 0.7 if success else 0.3,
'growth.new_areas_explored': 1,
'growth.last_experiment_findings': findings
},
execution_time=time.time() - start_time,
metadata={'type': experiment_type, 'success': success}
)
except Exception as e:
return ActionResult.failure_result(
error=str(e),
message=f"Experiment failed: {experiment_type}",
execution_time=time.time() - start_time
)
# =============================================================================
# ACTION LIBRARY
# =============================================================================
class ActionLibrary:
"""Registry of all available actions"""
def __init__(self):
self.actions: Dict[str, Action] = {}
self._register_default_actions()
def _register_default_actions(self):
"""Register all default actions"""
actions = [
# System actions
CheckSystemHealth(),
CleanupResources(),
RestartService(),
BackupData(),
# Knowledge actions
ResearchTopic(),
IndexKnowledge(),
LearnFromInteraction(),
# Skill actions
PracticeSkill(),
AcquireNewSkill(),
# Social actions
SendMessage(),
ProactiveCheckIn(),
ShareKnowledge(),
# Growth actions
SelfAnalysis(),
Experiment(),
]
for action in actions:
self.register(action)
def register(self, action: Action):
"""Register an action"""
self.actions[action.name] = action
def get(self, name: str) -> Optional[Action]:
"""Get an action by name"""
return self.actions.get(name)
def get_all(self) -> List[Action]:
"""Get all actions"""
return list(self.actions.values())
def get_by_category(self, category: str) -> List[Action]:
"""Get actions by category"""
return [a for a in self.actions.values() if a.category == category]
def get_applicable(self, world_state: Dict[str, Any]) -> List[Action]:
"""Get actions that can be applied in current world state"""
return [a for a in self.actions.values() if a.check_preconditions(world_state)]
def get_stats(self) -> Dict:
"""Get action library statistics"""
return {
'total_actions': len(self.actions),
'by_category': {
cat: len(self.get_by_category(cat))
for cat in set(a.category for a in self.actions.values())
},
'most_used': sorted(
self.actions.values(),
key=lambda a: a.execution_count,
reverse=True
)[:5],
'highest_success_rate': sorted(
self.actions.values(),
key=lambda a: a.success_count / max(1, a.execution_count),
reverse=True
)[:5]
}
def to_dict(self) -> Dict:
"""Export action library data"""
return {
'actions': {name: action.to_dict() for name, action in self.actions.items()},
'stats': self.get_stats()
}
# Singleton instance
action_library = ActionLibrary()
if __name__ == "__main__":
# Test the actions module
print("=== GOAP Actions Module Test ===")
library = ActionLibrary()
print("\n=== Available Actions ===")
for action in library.get_all():
print(f"- {action.name} (category: {action.category}, cost: {action.cost})")
print("\n=== Action Statistics ===")
stats = library.get_stats()
print(f"Total actions: {stats['total_actions']}")
print(f"By category: {stats['by_category']}")
# Test precondition checking
print("\n=== Precondition Test ===")
world_state = {
'system': {
'cpu_percent': 60,
'disk_percent': 75,
'memory_percent': 50
},
'services': {
'failing': ['test_service']
},
'knowledge': {
'concept_connections': 100
}
}
applicable = library.get_applicable(world_state)
print(f"Applicable actions in current state: {len(applicable)}")
for action in applicable:
print(f" - {action.name}")