Skip to content

Commit 19c5b9a

Browse files
fix: properly handle agent max iterations
fixes #3847
1 parent 1ed307b commit 19c5b9a

File tree

4 files changed

+560
-16
lines changed

4 files changed

+560
-16
lines changed

lib/crewai/src/crewai/agents/crew_agent_executor.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ def _invoke_loop(self) -> AgentFinish:
214214
llm=self.llm,
215215
callbacks=self.callbacks,
216216
)
217+
break
217218

218219
enforce_rpm_limit(self.request_within_rpm_limit)
219220

@@ -226,7 +227,7 @@ def _invoke_loop(self) -> AgentFinish:
226227
from_agent=self.agent,
227228
response_model=self.response_model,
228229
)
229-
formatted_answer = process_llm_response(answer, self.use_stop_words)
230+
formatted_answer = process_llm_response(answer, self.use_stop_words) # type: ignore[assignment]
230231

231232
if isinstance(formatted_answer, AgentAction):
232233
# Extract agent fingerprint if available
@@ -258,11 +259,11 @@ def _invoke_loop(self) -> AgentFinish:
258259
formatted_answer, tool_result
259260
)
260261

261-
self._invoke_step_callback(formatted_answer)
262-
self._append_message(formatted_answer.text)
262+
self._invoke_step_callback(formatted_answer) # type: ignore[arg-type]
263+
self._append_message(formatted_answer.text) # type: ignore[union-attr,attr-defined]
263264

264-
except OutputParserError as e: # noqa: PERF203
265-
formatted_answer = handle_output_parser_exception(
265+
except OutputParserError as e:
266+
formatted_answer = handle_output_parser_exception( # type: ignore[assignment]
266267
e=e,
267268
messages=self.messages,
268269
iterations=self.iterations,

lib/crewai/src/crewai/utilities/agent_utils.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def handle_max_iterations_exceeded(
127127
messages: list[LLMMessage],
128128
llm: LLM | BaseLLM,
129129
callbacks: list[TokenCalcHandler],
130-
) -> AgentAction | AgentFinish:
130+
) -> AgentFinish:
131131
"""Handles the case when the maximum number of iterations is exceeded. Performs one more LLM call to get the final answer.
132132
133133
Args:
@@ -139,7 +139,7 @@ def handle_max_iterations_exceeded(
139139
callbacks: List of callbacks for the LLM call.
140140
141141
Returns:
142-
The final formatted answer after exceeding max iterations.
142+
AgentFinish with the final answer after exceeding max iterations.
143143
"""
144144
printer.print(
145145
content="Maximum iterations reached. Requesting final answer.",
@@ -157,7 +157,7 @@ def handle_max_iterations_exceeded(
157157

158158
# Perform one more LLM call to get the final answer
159159
answer = llm.call(
160-
messages, # type: ignore[arg-type]
160+
messages,
161161
callbacks=callbacks,
162162
)
163163

@@ -168,8 +168,16 @@ def handle_max_iterations_exceeded(
168168
)
169169
raise ValueError("Invalid response from LLM call - None or empty.")
170170

171-
# Return the formatted answer, regardless of its type
172-
return format_answer(answer=answer)
171+
formatted = format_answer(answer=answer)
172+
173+
# If format_answer returned an AgentAction, convert it to AgentFinish
174+
if isinstance(formatted, AgentFinish):
175+
return formatted
176+
return AgentFinish(
177+
thought=formatted.thought,
178+
output=formatted.text,
179+
text=formatted.text,
180+
)
173181

174182

175183
def format_message_for_llm(
@@ -249,10 +257,10 @@ def get_llm_response(
249257
"""
250258
try:
251259
answer = llm.call(
252-
messages, # type: ignore[arg-type]
260+
messages,
253261
callbacks=callbacks,
254262
from_task=from_task,
255-
from_agent=from_agent,
263+
from_agent=from_agent, # type: ignore[arg-type]
256264
response_model=response_model,
257265
)
258266
except Exception as e:
@@ -294,8 +302,8 @@ def handle_agent_action_core(
294302
formatted_answer: AgentAction,
295303
tool_result: ToolResult,
296304
messages: list[LLMMessage] | None = None,
297-
step_callback: Callable | None = None,
298-
show_logs: Callable | None = None,
305+
step_callback: Callable | None = None, # type: ignore[type-arg]
306+
show_logs: Callable | None = None, # type: ignore[type-arg]
299307
) -> AgentAction | AgentFinish:
300308
"""Core logic for handling agent actions and tool results.
301309
@@ -481,7 +489,7 @@ def summarize_messages(
481489
),
482490
]
483491
summary = llm.call(
484-
messages, # type: ignore[arg-type]
492+
messages,
485493
callbacks=callbacks,
486494
)
487495
summarized_contents.append({"content": str(summary)})

lib/crewai/tests/agents/test_agent.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,47 @@ def counting_call(*args, **kwargs):
508508
assert isinstance(result, str)
509509
assert len(result) > 0
510510
assert call_count > 0
511-
assert call_count == 3
511+
# With max_iter=1, expect 2 calls:
512+
# - Call 1: iteration 0
513+
# - Call 2: iteration 1 (max reached, handle_max_iterations_exceeded called, then loop breaks)
514+
assert call_count == 2
515+
516+
517+
@pytest.mark.vcr(filter_headers=["authorization"])
518+
@pytest.mark.timeout(30)
519+
def test_agent_max_iterations_stops_loop():
520+
"""Test that agent execution terminates when max_iter is reached."""
521+
522+
@tool
523+
def get_data(step: str) -> str:
524+
"""Get data for a step. Always returns data requiring more steps."""
525+
return f"Data for {step}: incomplete, need to query more steps."
526+
527+
agent = Agent(
528+
role="data collector",
529+
goal="collect data using the get_data tool",
530+
backstory="You must use the get_data tool extensively",
531+
max_iter=2,
532+
allow_delegation=False,
533+
)
534+
535+
task = Task(
536+
description="Use get_data tool for step1, step2, step3, step4, step5, step6, step7, step8, step9, and step10. Do NOT stop until you've called it for ALL steps.",
537+
expected_output="A summary of all data collected",
538+
)
539+
540+
result = agent.execute_task(
541+
task=task,
542+
tools=[get_data],
543+
)
544+
545+
assert result is not None
546+
assert isinstance(result, str)
547+
548+
assert agent.agent_executor.iterations <= agent.max_iter + 2, (
549+
f"Agent ran {agent.agent_executor.iterations} iterations "
550+
f"but should stop around {agent.max_iter + 1}. "
551+
)
512552

513553

514554
@pytest.mark.vcr(filter_headers=["authorization"])

0 commit comments

Comments
 (0)