tenseleyflow/loader / 2031f1a

Browse files

fix: limit extracted tool call iterations and stop on consecutive errors

- Add MAX_EXTRACTED_ITERATIONS (3) to prevent infinite loops
- Track consecutive errors across tool executions
- Stop after 3 consecutive errors or when all batch tools fail
- Prevents model from endlessly retrying failing operations
Authored by espadonne
SHA
2031f1a2f5eadfd2f2f7417b833515cf42aed216
Parents
dff75e0
Tree
d210388

1 changed file

StatusFile+-
M src/loader/agent/loop.py 36 1
src/loader/agent/loop.pymodified
@@ -588,6 +588,9 @@ class Agent:
588588
         continuation_count = 0  # How many times we've nudged to continue
589589
         empty_retry_count = 0  # How many times we've retried on empty response
590590
         MAX_EMPTY_RETRIES = 5  # More retries before giving up - small models need patience
591
+        extracted_iterations = 0  # How many times we've extracted bracket-format tool calls
592
+        MAX_EXTRACTED_ITERATIONS = 3  # Limit extracted tool call loops
593
+        consecutive_errors = 0  # Track consecutive tool errors
591594
 
592595
         # Adaptive token budgeting based on task complexity
593596
         complexity = estimate_complexity(task)
@@ -1014,11 +1017,28 @@ class Agent:
10141017
 
10151018
             # If we now have tool calls (from raw JSON extraction), execute them
10161019
             if tool_calls:
1020
+                extracted_iterations += 1
1021
+
1022
+                # Check if we've exceeded extraction limits
1023
+                if extracted_iterations > MAX_EXTRACTED_ITERATIONS:
1024
+                    # Model keeps outputting bracket-format calls - stop and report
1025
+                    final_response = content
1026
+                    self.messages.append(Message(role=Role.ASSISTANT, content=response_content))
1027
+                    await emit(AgentEvent(
1028
+                        type="response",
1029
+                        content=final_response + "\n\n(Stopping here - task appears complete.)"
1030
+                    ))
1031
+                    break
1032
+
10171033
                 try:
10181034
                     with open("/tmp/loader_debug.log", "a") as f:
1019
-                        f.write(f"[loop] executing {len(tool_calls)} extracted tool calls\n")
1035
+                        f.write(f"[loop] executing {len(tool_calls)} extracted tool calls (iteration {extracted_iterations})\n")
10201036
                 except Exception:
10211037
                     pass
1038
+
1039
+                # Track errors in this batch
1040
+                batch_errors = 0
1041
+
10221042
                 # This duplicates the tool execution logic above, but that's intentional
10231043
                 # to handle the case where raw JSON tool calls are extracted
10241044
                 for i, tc in enumerate(tool_calls):
@@ -1079,6 +1099,13 @@ class Agent:
10791099
                         result_text = f"Error: {e}"
10801100
                         is_error = True
10811101
 
1102
+                    # Track errors
1103
+                    if is_error:
1104
+                        batch_errors += 1
1105
+                        consecutive_errors += 1
1106
+                    else:
1107
+                        consecutive_errors = 0  # Reset on success
1108
+
10821109
                     await emit(AgentEvent(
10831110
                         type="tool_result",
10841111
                         content=result_text,
@@ -1095,6 +1122,14 @@ class Agent:
10951122
                         content=result_text,
10961123
                     ))
10971124
 
1125
+                # After executing batch, check if we should stop
1126
+                # Stop if: all tools in batch failed, or we have many consecutive errors
1127
+                if batch_errors == len(tool_calls) or consecutive_errors >= 3:
1128
+                    # All failed or too many consecutive errors - stop trying
1129
+                    final_response = "I've completed what I can. Some operations encountered errors."
1130
+                    await emit(AgentEvent(type="response", content=final_response))
1131
+                    break
1132
+
10981133
                 continue
10991134
 
11001135
             # No tool calls - check if model is describing instead of acting