Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion lib/crewai/src/crewai/agents/agent_builder/base_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@
from crewai.state.runtime import RuntimeState


# Upper bound for agent iteration count. Prevents denial-of-wallet attacks
# where a misconfigured or malicious `max_iter` value (e.g. 10_000_000) would
# burn unbounded API credits. The default of 25 is unchanged; no legitimate
# workflow needs more than 500 iterations.
MAX_ITER_CEILING: Final[int] = 500


def _validate_crew_ref(value: Any) -> Any:
return value

Expand Down Expand Up @@ -284,7 +291,10 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
default_factory=list, description="Tools at agents' disposal"
)
max_iter: int = Field(
default=25, description="Maximum iterations for an agent to execute a task"
default=25,
gt=0,
le=MAX_ITER_CEILING,
description="Maximum iterations for an agent to execute a task",
)
agent_executor: Annotated[
SerializeAsAny[BaseAgentExecutor] | None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class BaseAgentExecutor(BaseModel):
agent: BaseAgent | None = Field(default=None, exclude=True)
task: Task | None = Field(default=None, exclude=True)
iterations: int = Field(default=0)
max_iter: int = Field(default=25)
max_iter: int = Field(default=25, gt=0, le=500)
messages: list[LLMMessage] = Field(default_factory=list)
_resuming: bool = PrivateAttr(default=False)

Expand Down
2 changes: 1 addition & 1 deletion lib/crewai/src/crewai/experimental/agent_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
prompt: SystemPromptResult | StandardPromptResult | None = Field(
default=None, exclude=True
)
max_iter: int = Field(default=25, exclude=True)
max_iter: int = Field(default=25, gt=0, le=500, exclude=True)
tools: list[CrewStructuredTool] = Field(default_factory=list, exclude=True)
tools_names: str = Field(default="", exclude=True)
stop_words: list[str] = Field(default_factory=list, exclude=True)
Expand Down
15 changes: 15 additions & 0 deletions lib/crewai/src/crewai/flow/runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
Annotated,
Any,
ClassVar,
Final,
Generic,
Literal,
ParamSpec,
Expand Down Expand Up @@ -166,6 +167,13 @@
logger = logging.getLogger(__name__)


# Upper bound on router hops within a single _route() invocation. Guards
# against circular @router definitions that would otherwise spin the event
# loop indefinitely. No legitimate flow exceeds this; if hit, surface a
# RuntimeError so the cycle is diagnosable instead of silently hanging.
MAX_ROUTER_HOPS: Final[int] = 100


def _condition_branches(
condition: dict[str, Any],
) -> tuple[Literal["and", "or"], list[FlowDefinitionCondition]]:
Expand Down Expand Up @@ -2734,8 +2742,15 @@ async def _execute_listeners(
current_trigger = trigger_method
current_result = result # Track the result to pass to each router
current_triggering_event_id = triggering_event_id
hop_count = 0

while True:
hop_count += 1
if hop_count > MAX_ROUTER_HOPS:
raise RuntimeError(
f"Flow router cycle detected: exceeded {MAX_ROUTER_HOPS} "
"router hops. Check for circular @router definitions."
)
Comment on lines +2745 to +2753

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Map the AgentExecutor router graph and confirm which routers can chain
# back into one another within a single _execute_listeners pass.
fd -t f 'agent_executor.py' lib/crewai/src/crewai/experimental \
  --exec rg -nP '`@router`\(|return "(initialized|continue_reasoning|todo_not_satisfied|check_iteration|parsed)"' {}

# Check for any existing test exercising many iterations through the flow router loop.
rg -nP --type=py -C2 'MAX_ROUTER_HOPS|max_iter\s*=\s*(1\d\d|[2-9]\d\d)' lib/crewai

Repository: crewAIInc/crewAI

Length of output: 2879


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the router functions involved in the AgentExecutor loop and the runtime hop counter.
sed -n '1320,1455p' lib/crewai/src/crewai/flow/runtime/__init__.py
printf '\n---\n'
sed -n '2050,2245p' lib/crewai/src/crewai/flow/runtime/__init__.py
printf '\n---\n'
sed -n '2660,2715p' lib/crewai/src/crewai/flow/runtime/__init__.py
printf '\n---\n'
sed -n '2735,2760p' lib/crewai/src/crewai/flow/runtime/__init__.py

Repository: crewAIInc/crewAI

Length of output: 18309


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the max-iteration configuration and the class that uses the router loop.
rg -n -C3 'max_iter|max_method_calls|MAX_ROUTER_HOPS|increment_and_continue|check_max_iterations|continue_reasoning|route_by_answer_type|check_todo_completion' lib/crewai/src/crewai/flow/runtime/__init__.py

Repository: crewAIInc/crewAI

Length of output: 1783


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the router bodies that form the suspected cycle.
sed -n '1338,1395p' lib/crewai/src/crewai/flow/runtime/__init__.py
printf '\n---\n'
sed -n '2088,2145p' lib/crewai/src/crewai/flow/runtime/__init__.py
printf '\n---\n'
sed -n '2234,2245p' lib/crewai/src/crewai/flow/runtime/__init__.py
printf '\n---\n'
sed -n '2648,2715p' lib/crewai/src/crewai/flow/runtime/__init__.py

Repository: crewAIInc/crewAI

Length of output: 8488


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for the agent-side max_iter default/configuration.
rg -n -C2 'max_iter\s*[:=]\s*500|max_iter\s*[:=]\s*\d+|max_iter' lib/crewai/src/crewai/agents lib/crewai/src/crewai/flow lib/crewai/src/crewai

Repository: crewAIInc/crewAI

Length of output: 25918


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the agent loop routers and the surrounding methods in the experimental executor.
sed -n '2080,2255p' lib/crewai/src/crewai/experimental/agent_executor.py
printf '\n---\n'
sed -n '2230,2305p' lib/crewai/src/crewai/experimental/agent_executor.py

Repository: crewAIInc/crewAI

Length of output: 10204


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show where the experimental executor re-enters listener execution so we can tell whether
# the router chain is bounded per method/result or across the whole agent run.
rg -n -C3 '_execute_listeners\(' lib/crewai/src/crewai/experimental/agent_executor.py

Repository: crewAIInc/crewAI

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the missing router methods around the iteration/reasoning loop.
sed -n '1360,1465p' lib/crewai/src/crewai/experimental/agent_executor.py
printf '\n---\n'
sed -n '1465,1615p' lib/crewai/src/crewai/experimental/agent_executor.py
printf '\n---\n'
sed -n '1615,1775p' lib/crewai/src/crewai/experimental/agent_executor.py
printf '\n---\n'
sed -n '1775,1915p' lib/crewai/src/crewai/experimental/agent_executor.py

Repository: crewAIInc/crewAI

Length of output: 21862


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find every router/listener in the experimental executor that can feed back into the loop.
rg -n -C2 '`@router`\(|`@listen`\(' lib/crewai/src/crewai/experimental/agent_executor.py

Repository: crewAIInc/crewAI

Length of output: 5730


MAX_ROUTER_HOPS is too low for the experimental agent loop Consider raising it or deriving it from max_iter/max_method_calls; the current 100-hop cap can trip valid runs before they exhaust the 500-iteration limit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/flow/runtime/__init__.py` around lines 2745 - 2753, The
router hop cap in the flow runtime is too low for valid experimental agent
loops, so update the MAX_ROUTER_HOPS check in the router loop to allow longer
runs. Either raise the static limit or derive it from the existing
max_iter/max_method_calls settings used by the flow runtime so the cycle guard
stays consistent with those limits. Keep the change localized around the
hop_count validation and the router cycle detection logic.

routers_triggered = self._find_triggered_methods(
current_trigger, router_only=True
)
Expand Down