Skip to content
Open
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
22 changes: 19 additions & 3 deletions lib/crewai/src/crewai/utilities/reasoning_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import json
import logging
import re
from typing import TYPE_CHECKING, Any, Final, Literal, cast

from pydantic import BaseModel, Field
Expand Down Expand Up @@ -411,7 +412,7 @@ def _create_reasoning_plan(
return (
response_str,
[],
"READY: I am ready to execute the task." in response_str,
AgentReasoning._is_plan_ready(response_str),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="lib/crewai/src/crewai/utilities/reasoning_handler.py"

echo "== file exists and line count =="
wc -l "$file"

echo "== outline around relevant symbols =="
ast-grep outline "$file" --match 'AgentReasoning' --view expanded || true

echo "== relevant line slices =="
for range in "90-140" "380-445" "550-625"; do
  echo "--- ${file}:${range} ---"
  sed -n "${range}p" "$file" | nl -ba -v "${range#*-}"
done

echo "== searches for _is_plan_ready and ready regex =="
rg -n "_is_plan_ready|ready|NOT\s+READY|NOT\ \+READY|ready\b" "$file"

Repository: crewAIInc/crewAI

Length of output: 1632


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="lib/crewai/src/crewai/utilities/reasoning_handler.py"

python3 - <<'PY'
from pathlib import Path
file = Path("lib/crewai/src/crewai/utilities/reasoning_handler.py")
text = file.read_text()
for start, end in [(90,140), (240,320), (380,450), (550,630)]:
    print(f"--- {file}:{start}-{end} ---")
    for i, line in enumerate(text.splitlines(), 1):
        if start <= i <= end:
            print(f"{i:4d}\t{line}")
print("--- searches ---")
for pat in ["_is_plan_ready", "ready", "NOT", "\\bnot", "parse_planning_response"]:
    print(f"-- {pat} --")
    for i, line in enumerate(text.splitlines(), 1):
        if pat.lower() in line.lower() or pat in line:
            print(f"{i:4d}\t{line}")
PY

echo "== behavioral probe from source regex =="
python3 - <<'PY'
import ast
from pathlib import Path

file = Path("lib/crewai/src/crewai/utilities/reasoning_handler.py")
source = file.read_text()
tree = ast.parse(source, filename=str(file))

compiled_re = None
for node in ast.walk(tree):
    if isinstance(node, ast.AnnAssign):
        annot = node.target.id if isinstance(node.target, ast.Name) else None
        if annot == "plan_ready_regex":
            compiled_re = ast.literal_eval(node.value)
            break

print("compiled_regex_string:", repr(compiled_re))

import re
if compiled_re is not None:
    pattern = re.compile(compiled_re, re.IGNORECASE)
    cases = [
        "READY",
        "I'm ready!",
        "NOT READY",
        "NOT  READY",
        '{"ready": false}',
        '{"ready": true}',
        "not ready",
    ]
    for case in cases:
        m = pattern.search(case)
        print(repr(case), "=>", m.group() if m else None)
PY

Repository: crewAIInc/crewAI

Length of output: 16209


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import re

pattern = re.compile(r"(?<!\bnot\s)\bready\b", re.IGNORECASE)
cases = [
    "READY",
    "I'm ready!",
    "NOT READY",
    "NOT  READY",
    '{"ready": false}',
    '{"ready": true}',
    "not ready",
    "not  ready",
    "ready",
]
for case in cases:
    m = pattern.search(case)
    print(f"{case!r} => {m.group()!r if m else None}")
PY

Repository: crewAIInc/crewAI

Length of output: 327


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import re

pattern = re.compile(r"(?<!\bnot\s)\bready\b", re.IGNORECASE)
cases = [
    "READY",
    "I'm ready!",
    "NOT READY",
    "NOT  READY",
    '{"ready": false}',
    '{"ready": true}',
    "not ready",
    "not  ready",
    "ready",
]
for case in cases:
    m = pattern.search(case)
    print(repr(case), "=>", m.group() if m else None)
PY

Repository: crewAIInc/crewAI

Length of output: 349


Avoid treating every ready token as a readiness declaration.

The fallback paths pass the response text directly into _is_plan_ready, where "{"ready": false}" matches the JSON field name and NOT READY/not ready matches because the lookbehind only handles one whitespace. This can skip refinement for non-ready plans. Preserve parsed structured ready booleans, exclude JSON field names, match one-or-more whitespace after not, and add regression tests for READY, I'm ready!, NOT READY, NOT READY, and {"ready": false}.

🤖 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/utilities/reasoning_handler.py` at line 413, Update
AgentReasoning._is_plan_ready and its fallback callers to recognize readiness
declarations without treating JSON field names as readiness: preserve structured
parsed ready booleans, exclude matches for the "ready" JSON key, and require
one-or-more whitespace after "not" so both NOT READY and NOT  READY are
negative. Add regression coverage for READY, I'm ready!, NOT READY, NOT  READY,
and {"ready": false}.

)

except HookAborted:
Expand All @@ -437,7 +438,7 @@ def _create_reasoning_plan(
return (
fallback_str,
[],
"READY: I am ready to execute the task." in fallback_str,
AgentReasoning._is_plan_ready(fallback_str),
)
except HookAborted:
raise
Expand Down Expand Up @@ -585,6 +586,21 @@ def _create_refine_prompt(self, current_plan: str) -> str:
current_plan=current_plan,
)

@staticmethod
def _is_plan_ready(response: str) -> bool:
"""Check if the agent indicated readiness in the planning response.

Detects standalone ``READY`` that is not part of ``NOT READY``.
Case-insensitive to handle ``Ready``, ``READY``, ``ready``, etc.

Args:
response: The LLM response text.

Returns:
True if the agent declared READY, False otherwise.
"""
return bool(re.search(r"(?<!\bnot\s)\bready\b", response, re.IGNORECASE))

@staticmethod
def _parse_planning_response(response: str) -> tuple[str, bool]:
"""Parses the planning response to extract the plan and readiness.
Expand All @@ -599,7 +615,7 @@ def _parse_planning_response(response: str) -> tuple[str, bool]:
return "No plan was generated.", False

plan = response
ready = "READY: I am ready to execute the task." in response
ready = AgentReasoning._is_plan_ready(response)

return plan, ready

Expand Down