From ec4aee8088eefa75ddb70421c8f34b4c2bf1f197 Mon Sep 17 00:00:00 2001 From: Aidyn Torgayev Date: Wed, 4 Mar 2026 00:05:02 +0500 Subject: [PATCH] fix: prevent executor agent from getting stuck in action loop (#251) - Add critical rules to executor system prompt: use wait as fallback when no viable action exists, always output a valid action, try different approaches after repeated failures - Add repeated-action loop detection in handle_executor_result: when the same action is repeated 3+ times consecutively, set error_flag_plan to force the manager to re-plan --- droidrun/agent/droid/droid_agent.py | 17 ++++++++++++++++- droidrun/config/prompts/executor/system.jinja2 | 4 +++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/droidrun/agent/droid/droid_agent.py b/droidrun/agent/droid/droid_agent.py index 9f5dda2b..02a414ce 100644 --- a/droidrun/agent/droid/droid_agent.py +++ b/droidrun/agent/droid/droid_agent.py @@ -7,6 +7,7 @@ - When reasoning=True: Uses Manager (planning) + Executor (action) workflows """ +import json import logging from typing import TYPE_CHECKING, Type, Awaitable, Union @@ -762,9 +763,23 @@ async def handle_executor_result( """ Process Executor result and continue. - Checks for error escalation and loops back to Manager. + Checks for error escalation, repeated action loops, and loops back to Manager. Note: Max steps check is now done in run_manager pre-flight. """ + # Check for repeated action loop (same action N times in a row) + repeat_thresh = 3 + if len(self.shared_state.action_pool) >= repeat_thresh: + recent_actions = self.shared_state.action_pool[-repeat_thresh:] + try: + normalized = [json.dumps(a, sort_keys=True) for a in recent_actions] + if len(set(normalized)) == 1: + logger.warning( + f"⚠️ Executor stuck: same action repeated {repeat_thresh} times in a row: {normalized[0]}" + ) + self.shared_state.error_flag_plan = True + except (TypeError, ValueError): + pass # Non-serializable actions, skip detection + # Check error escalation and reset flag when errors are resolved err_thresh = self.shared_state.err_to_manager_thresh diff --git a/droidrun/config/prompts/executor/system.jinja2 b/droidrun/config/prompts/executor/system.jinja2 index d8d41d2e..ff357e9d 100644 --- a/droidrun/config/prompts/executor/system.jinja2 +++ b/droidrun/config/prompts/executor/system.jinja2 @@ -93,8 +93,10 @@ No actions have been taken yet. Whatever the current subgoal says to do, do that EXACTLY. Do not substitute with what you think is better. Do not optimize. Do not consider screen state. Parse the subgoal text literally and execute the matching atomic action. IMPORTANT: -1. Do NOT repeat previously failed actions multiple times. Try changing to another action. +1. Do NOT repeat previously failed actions multiple times. If an action failed, try a DIFFERENT action or approach. 2. Must do the current subgoal. +3. If you have tried the same action 2+ times and it keeps failing, try a completely different approach. If truly stuck with no viable action, use `{"action": "wait", "duration": 1.0}` as a fallback and explain why in the Description (e.g., "No actionable element found for subgoal"). +4. ALWAYS output a valid action. There is no "skip" or "do nothing" option — use `wait` with duration 1.0 if uncertain. Provide your output in the following format, which contains three parts: