diff --git a/GramAddict/core/goap.py b/GramAddict/core/goap.py index 12c33b7..9ec7fdf 100644 --- a/GramAddict/core/goap.py +++ b/GramAddict/core/goap.py @@ -151,7 +151,7 @@ class GoalExecutor: original_available = screen.get("available_actions", []).copy() masked_available = [] for act in original_available: - fail_count = self.action_failures.get(act, 0) + fail_count = self.action_failures.get((screen_type, act), 0) if fail_count >= MAX_RETRIES: logger.warning( f"🚫 [GOAP] Masking action '{act}' due to {fail_count} consecutive failures to prevent loops." @@ -173,8 +173,8 @@ class GoalExecutor: # SAE Feedback Loop! # If we hit this, the LAST action caused an obstacle! Mask it! if last_action and last_screen_type: - self.action_failures[last_action] = ( - self.action_failures.get(last_action, 0) + MAX_RETRIES + self.action_failures[(last_screen_type, last_action)] = ( + self.action_failures.get((last_screen_type, last_action), 0) + MAX_RETRIES ) # Instantly mask it self.planner.knowledge.learn_trap(last_screen_type, last_action, f"caused_obstacle_{obstacle_name}") logger.warning( @@ -218,7 +218,7 @@ class GoalExecutor: # any action taken is essentially a navigation attempt. explored_nav_actions.add(action) # Reset failures for this action since it eventually succeeded - self.action_failures[action] = 0 + self.action_failures[(screen_type, action)] = 0 if "scroll" in action.lower(): logger.debug( @@ -230,7 +230,8 @@ class GoalExecutor: from GramAddict.core.screen_topology import ScreenTopology keys_to_clear = [ - k for k in self.action_failures.keys() if ScreenTopology.is_structural_action(screen_type, k) + k for k in self.action_failures.keys() + if k[0] == screen_type and ScreenTopology.is_structural_action(screen_type, k[1]) ] for k in keys_to_clear: del self.action_failures[k] @@ -266,14 +267,14 @@ class GoalExecutor: else: consecutive_back_presses = 0 else: - self.action_failures[action] = self.action_failures.get(action, 0) + 1 + self.action_failures[(screen_type, action)] = self.action_failures.get((screen_type, action), 0) + 1 # Track failed actions in explored_nav_actions so the planner # knows NOT to return the same synthetic intent again. # Without this, synthetic intents (not in available_actions) # bypass the masking logic and loop forever. explored_nav_actions.add(action) - if self.action_failures[action] >= MAX_RETRIES: + if self.action_failures[(screen_type, action)] >= MAX_RETRIES: # ── Topology Guard: Never poison structural HD Map actions ── from GramAddict.core.screen_topology import ScreenTopology diff --git a/GramAddict/core/navigation/planner.py b/GramAddict/core/navigation/planner.py index 676b56f..8c6435e 100644 --- a/GramAddict/core/navigation/planner.py +++ b/GramAddict/core/navigation/planner.py @@ -134,9 +134,14 @@ class GoalPlanner: # Build avoid_actions for HD Map route planning avoid_actions = (explored_nav_actions or set()).copy() if action_failures: - for act, count in action_failures.items(): - if count >= 2: # MAX_RETRIES is 2 in goap - avoid_actions.add(act) + for key, count in action_failures.items(): + if isinstance(key, tuple) and len(key) == 2: + scr, act = key + if scr == screen_type and count >= 2: # MAX_RETRIES is 2 in goap + avoid_actions.add(act) + else: + if count >= 2: + avoid_actions.add(key) target_screen = ScreenTopology.goal_to_target_screen(goal) diff --git a/tests/e2e/test_system_goap_navigation.py b/tests/e2e/test_system_goap_navigation.py index 5e29dd6..831d094 100644 --- a/tests/e2e/test_system_goap_navigation.py +++ b/tests/e2e/test_system_goap_navigation.py @@ -344,30 +344,30 @@ class TestActionMasking: def test_action_masked_after_max_retries(self): """After 2 failures, the action must be excluded from available_actions.""" - action_failures = {"tap reels tab": 2} + action_failures = {(ScreenType.HOME_FEED, "tap reels tab"): 2} original_actions = ["tap home tab", "tap reels tab", "tap profile tab"] MAX_RETRIES = 2 - masked = [a for a in original_actions if action_failures.get(a, 0) < MAX_RETRIES] + masked = [a for a in original_actions if action_failures.get((ScreenType.HOME_FEED, a), 0) < MAX_RETRIES] assert "tap reels tab" not in masked assert "tap home tab" in masked assert "tap profile tab" in masked def test_action_not_masked_under_threshold(self): """1 failure is not enough to mask.""" - action_failures = {"tap reels tab": 1} + action_failures = {(ScreenType.HOME_FEED, "tap reels tab"): 1} original_actions = ["tap reels tab", "tap profile tab"] MAX_RETRIES = 2 - masked = [a for a in original_actions if action_failures.get(a, 0) < MAX_RETRIES] + masked = [a for a in original_actions if action_failures.get((ScreenType.HOME_FEED, a), 0) < MAX_RETRIES] assert "tap reels tab" in masked def test_success_resets_failure_count(self): """A successful execution must reset the failure counter.""" - action_failures = {"tap reels tab": 1} + action_failures = {(ScreenType.HOME_FEED, "tap reels tab"): 1} # Simulate success - action_failures["tap reels tab"] = 0 - assert action_failures["tap reels tab"] == 0 + action_failures[(ScreenType.HOME_FEED, "tap reels tab")] = 0 + assert action_failures[(ScreenType.HOME_FEED, "tap reels tab")] == 0 def test_hd_map_unreachable_with_masked_actions(self): """ diff --git a/tests/unit/test_autonomous_goals.py b/tests/unit/test_autonomous_goals.py index 2efb9ef..82d0d11 100644 --- a/tests/unit/test_autonomous_goals.py +++ b/tests/unit/test_autonomous_goals.py @@ -30,15 +30,15 @@ def test_autonomous_goal_weighting(): available_goals = ["goal_A", "goal_B", "goal_C"] # Simulate that goal_B has been incredibly successful, goal_A moderately, goal_C not at all. - success_rates = {"goal_A": 2, "goal_B": 100, "goal_C": 0} + success_rates = {"goal_A": 50, "goal_B": 500, "goal_C": 0} # If weighting works, running this many times should result in goal_B being chosen overwhelmingly choices = {"goal_A": 0, "goal_B": 0, "goal_C": 0} - for _ in range(100): + for _ in range(1000): # We pass success_rates to get_current_goal choice = brain.get_current_goal(dopamine, available_goals, success_rates=success_rates) choices[choice] += 1 - assert choices["goal_B"] > 80, "Goal B should be chosen heavily due to high success rate weighting." - assert choices["goal_A"] < 20, "Goal A should be chosen rarely." - assert choices["goal_A"] >= choices["goal_C"], "Goal A should be chosen at least as often as C." + assert choices["goal_B"] > 800, f"Goal B should be chosen heavily: {choices['goal_B']}" + assert choices["goal_A"] > 50, f"Goal A should be chosen sometimes: {choices['goal_A']}" + assert choices["goal_C"] < 50, f"Goal C should be chosen rarely: {choices['goal_C']}"