BUG-HUNT: [spec-alignment] MaxIterationsExceededError is defined but never raised — run_tool_loop() silently returns empty content instead of raising the documented error #6589

Open
opened 2026-04-09 21:52:10 +00:00 by HAL9000 · 0 comments
Owner

Bug Report: [spec-alignment] — MaxIterationsExceededError defined but never raised; callers cannot distinguish iteration-limit termination from normal completion

Severity Assessment

  • Impact: Callers of ToolCallingRuntime.run_tool_loop() who rely on catching MaxIterationsExceededError (as implied by the class definition and module docstring) will never receive it. The loop silently returns a ToolCallRunResult with content="" and terminated_by_limit=True, which callers may not check, causing silent incomplete work to be treated as successful completion.
  • Likelihood: High — any code that calls run_tool_loop() expecting work to complete could receive empty content and not detect the failure.
  • Priority: High

Location

  • File: src/cleveragents/tool/actor_runtime.py
  • Class: MaxIterationsExceededError, ToolCallingRuntime.run_tool_loop
  • Lines: 64–65 (error class), 505–524 (loop termination logic)

Description

MaxIterationsExceededError is defined at line 64:

class MaxIterationsExceededError(Exception):
    """Raised when the tool-call loop exceeds the configured iteration limit."""

The module docstring says:

## Safety
- ``max_iterations`` (default 25) prevents infinite loops
- Each iteration is tracked; exceeding the limit produces a
  ``MaxIterationsExceededError``

However, the actual loop termination code at lines 505–524 never raises this error:

# Max iterations reached
logger.warning(
    "Tool-call loop reached max iterations (%d)", self._max_iterations
)
# Emit ACTOR_ESCALATED when max iterations reached
self._try_emit(
    EventType.ACTOR_ESCALATED,
    context.plan_id,
    {
        "plan_id": context.plan_id,
        "reason": "max_iterations_reached",
        "max_iterations": self._max_iterations,
    },
)
return ToolCallRunResult(    # <-- silently returns, no exception
    content=final_content,                    # final_content = "" (initial value, never updated after last tool-call iteration)
    tool_call_history=context.tool_call_history,
    iterations=self._max_iterations,
    terminated_by_limit=True,
)

Additionally, final_content is initialized to "" on line 463 and is only updated when the LLM returns a response without tool calls (line 482). When the loop hits the iteration limit, the last LLM response still had tool calls (that's why the loop didn't terminate), so final_content remains "". The returned content="" is therefore always empty when terminated_by_limit=True.

Expected Behavior

Per the documented API and the MaxIterationsExceededError class definition, run_tool_loop() should raise MaxIterationsExceededError when the limit is reached. This forces callers to explicitly handle the incomplete work case.

Actual Behavior

When max_iterations is reached, the method silently returns:

ToolCallRunResult(content="", tool_call_history=[...], iterations=25, terminated_by_limit=True)

Callers who don't check terminated_by_limit will treat the empty content as a valid (completed) response.

Suggested Fix

Either raise MaxIterationsExceededError as documented, or clearly document that the error is NOT raised and callers must check terminated_by_limit:

Option A (raise exception — matches docs):

# Max iterations reached
logger.warning("Tool-call loop reached max iterations (%d)", self._max_iterations)
self._try_emit(...)
raise MaxIterationsExceededError(
    f"Tool-call loop exceeded {self._max_iterations} iterations for plan "
    f"'{context.plan_id}'. Last tool calls: "
    f"{[r.tool_name for r in context.tool_call_history[-3:]]}"
)

Option B (keep return, update docstring + remove misleading error class):
Remove MaxIterationsExceededError from the public API or document it as "defined for future use" and clearly document that terminated_by_limit=True is the signal for callers.

Category

spec-alignment

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: @tdd_issue, @tdd_issue_<this-issue-number>, and @tdd_expected_fail to prove the bug exists before fixing it.


Automated by CleverAgents Bot
Supervisor: Bug Hunting | Agent: bug-hunter

## Bug Report: [spec-alignment] — `MaxIterationsExceededError` defined but never raised; callers cannot distinguish iteration-limit termination from normal completion ### Severity Assessment - **Impact**: Callers of `ToolCallingRuntime.run_tool_loop()` who rely on catching `MaxIterationsExceededError` (as implied by the class definition and module docstring) will never receive it. The loop silently returns a `ToolCallRunResult` with `content=""` and `terminated_by_limit=True`, which callers may not check, causing silent incomplete work to be treated as successful completion. - **Likelihood**: High — any code that calls `run_tool_loop()` expecting work to complete could receive empty content and not detect the failure. - **Priority**: High ### Location - **File**: `src/cleveragents/tool/actor_runtime.py` - **Class**: `MaxIterationsExceededError`, `ToolCallingRuntime.run_tool_loop` - **Lines**: 64–65 (error class), 505–524 (loop termination logic) ### Description `MaxIterationsExceededError` is defined at line 64: ```python class MaxIterationsExceededError(Exception): """Raised when the tool-call loop exceeds the configured iteration limit.""" ``` The module docstring says: ``` ## Safety - ``max_iterations`` (default 25) prevents infinite loops - Each iteration is tracked; exceeding the limit produces a ``MaxIterationsExceededError`` ``` However, the actual loop termination code at lines 505–524 **never raises** this error: ```python # Max iterations reached logger.warning( "Tool-call loop reached max iterations (%d)", self._max_iterations ) # Emit ACTOR_ESCALATED when max iterations reached self._try_emit( EventType.ACTOR_ESCALATED, context.plan_id, { "plan_id": context.plan_id, "reason": "max_iterations_reached", "max_iterations": self._max_iterations, }, ) return ToolCallRunResult( # <-- silently returns, no exception content=final_content, # final_content = "" (initial value, never updated after last tool-call iteration) tool_call_history=context.tool_call_history, iterations=self._max_iterations, terminated_by_limit=True, ) ``` Additionally, `final_content` is initialized to `""` on line 463 and is only updated when the LLM returns a response **without** tool calls (line 482). When the loop hits the iteration limit, the last LLM response still had tool calls (that's why the loop didn't terminate), so `final_content` remains `""`. The returned `content=""` is therefore always empty when `terminated_by_limit=True`. ### Expected Behavior Per the documented API and the `MaxIterationsExceededError` class definition, `run_tool_loop()` should raise `MaxIterationsExceededError` when the limit is reached. This forces callers to explicitly handle the incomplete work case. ### Actual Behavior When `max_iterations` is reached, the method silently returns: ```python ToolCallRunResult(content="", tool_call_history=[...], iterations=25, terminated_by_limit=True) ``` Callers who don't check `terminated_by_limit` will treat the empty content as a valid (completed) response. ### Suggested Fix Either raise `MaxIterationsExceededError` as documented, or clearly document that the error is NOT raised and callers must check `terminated_by_limit`: **Option A (raise exception — matches docs):** ```python # Max iterations reached logger.warning("Tool-call loop reached max iterations (%d)", self._max_iterations) self._try_emit(...) raise MaxIterationsExceededError( f"Tool-call loop exceeded {self._max_iterations} iterations for plan " f"'{context.plan_id}'. Last tool calls: " f"{[r.tool_name for r in context.tool_call_history[-3:]]}" ) ``` **Option B (keep return, update docstring + remove misleading error class):** Remove `MaxIterationsExceededError` from the public API or document it as "defined for future use" and clearly document that `terminated_by_limit=True` is the signal for callers. ### Category `spec-alignment` ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: `@tdd_issue`, `@tdd_issue_<this-issue-number>`, and `@tdd_expected_fail` to prove the bug exists before fixing it. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: bug-hunter
HAL9000 added this to the v3.2.0 milestone 2026-04-09 22:13:20 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#6589
No description provided.