From 4b9965124be4dc79e38a01cf53c1ea3d91c46bc3 Mon Sep 17 00:00:00 2001 From: HumphreySun98 Date: Wed, 8 Jul 2026 15:45:34 -0500 Subject: [PATCH 1/2] fix(reasoning): detect a bare READY marker in text plans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The refine prompt (and one of the create prompts) tells the model to "Conclude with READY or NOT READY", so models — especially on the text-parsing path — emit a bare `READY`. But the parser only accepted the exact sentence "READY: I am ready to execute the task.", so `READY` was never detected as ready and the agent stayed stuck refining until max attempts. Add a _detect_plan_ready helper that matches the last READY / NOT READY marker (NOT READY wins over the bare READY it contains; the full sentence still counts as ready) and use it at all three text-detection sites. Fixes #6204 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/crewai/utilities/reasoning_handler.py | 37 +++++++++++++++++-- .../utilities/test_structured_planning.py | 32 ++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/lib/crewai/src/crewai/utilities/reasoning_handler.py b/lib/crewai/src/crewai/utilities/reasoning_handler.py index 1028a3f3de..15ebaffdb2 100644 --- a/lib/crewai/src/crewai/utilities/reasoning_handler.py +++ b/lib/crewai/src/crewai/utilities/reasoning_handler.py @@ -4,6 +4,7 @@ import json import logging +import re from typing import TYPE_CHECKING, Any, Final, Literal, cast from pydantic import BaseModel, Field @@ -27,6 +28,36 @@ from crewai.task import Task +_READY_MARKER: Final[re.Pattern[str]] = re.compile( + r"NOT\s+READY|READY", re.IGNORECASE +) + + +def _detect_plan_ready(text: str) -> bool: + """Detect whether a text reasoning plan concludes with READY. + + The reasoning prompts instruct the model to conclude with "READY" or + "NOT READY", but models (especially on the text-parsing path) often emit a + bare ``READY`` rather than the full "READY: I am ready to execute the task." + sentence, which the previous exact-substring check missed (#6204). + + Matches the LAST readiness marker so a "not ready" mentioned earlier in the + plan doesn't override a final "READY", and so "NOT READY" wins over the bare + "READY" it contains. + + Args: + text: The raw model response. + + Returns: + True if the concluding marker is READY, False for NOT READY or if no + marker is present. + """ + markers = _READY_MARKER.findall(text) + if not markers: + return False + return "NOT" not in markers[-1].upper() + + class ReasoningPlan(BaseModel): """Model representing a reasoning plan for a task.""" @@ -409,7 +440,7 @@ def _create_reasoning_plan( return ( response_str, [], - "READY: I am ready to execute the task." in response_str, + _detect_plan_ready(response_str), ) except Exception as e: @@ -433,7 +464,7 @@ def _create_reasoning_plan( return ( fallback_str, [], - "READY: I am ready to execute the task." in fallback_str, + _detect_plan_ready(fallback_str), ) except Exception as inner_e: self.logger.error(f"Error during fallback text parsing: {inner_e!s}") @@ -593,7 +624,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 = _detect_plan_ready(response) return plan, ready diff --git a/lib/crewai/tests/utilities/test_structured_planning.py b/lib/crewai/tests/utilities/test_structured_planning.py index 1e36c6de97..92df3c0931 100644 --- a/lib/crewai/tests/utilities/test_structured_planning.py +++ b/lib/crewai/tests/utilities/test_structured_planning.py @@ -17,9 +17,41 @@ FUNCTION_SCHEMA, AgentReasoning, ReasoningPlan, + _detect_plan_ready, ) +class TestDetectPlanReady: + """Readiness detection must accept a bare READY marker, not only the full + 'READY: I am ready to execute the task.' sentence (#6204).""" + + def test_bare_ready_marker_is_ready(self): + assert _detect_plan_ready("1. do X\n2. do Y\n\n---\n\nREADY") is True + + def test_full_sentence_still_ready(self): + assert _detect_plan_ready("plan...\nREADY: I am ready to execute the task.") + + def test_not_ready_marker_is_not_ready(self): + assert ( + _detect_plan_ready("plan...\n\nNOT READY: I need more detail.") is False + ) + + def test_last_marker_wins_over_mid_text_not_ready(self): + text = "Step 1 is not ready to run yet without input.\n\nREADY" + assert _detect_plan_ready(text) is True + + def test_case_insensitive(self): + assert _detect_plan_ready("plan...\n\nready") is True + assert _detect_plan_ready("plan...\n\nnot ready") is False + + def test_no_marker_defaults_to_not_ready(self): + assert _detect_plan_ready("just a plan with no verdict") is False + + def test_parse_planning_response_detects_bare_ready(self): + _plan, ready = AgentReasoning._parse_planning_response("my plan\n\nREADY") + assert ready is True + + class TestFunctionSchema: """Tests for the FUNCTION_SCHEMA used in structured planning.""" From 24d34f6edcd672d471ca6780f663b93541776b2d Mon Sep 17 00:00:00 2001 From: HumphreySun98 Date: Sat, 11 Jul 2026 12:50:08 -0500 Subject: [PATCH 2/2] fix(reasoning): word-boundary the READY markers to avoid false positives Without word boundaries the pattern matched "ready" inside words like "already", so plain prose ("I am already prepared") registered as READY, and a trailing "already" could even flip a concluding NOT READY to ready. Anchor both markers with \b and add regression tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/crewai/src/crewai/utilities/reasoning_handler.py | 2 +- .../tests/utilities/test_structured_planning.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/crewai/src/crewai/utilities/reasoning_handler.py b/lib/crewai/src/crewai/utilities/reasoning_handler.py index 15ebaffdb2..5ddef4aad1 100644 --- a/lib/crewai/src/crewai/utilities/reasoning_handler.py +++ b/lib/crewai/src/crewai/utilities/reasoning_handler.py @@ -29,7 +29,7 @@ _READY_MARKER: Final[re.Pattern[str]] = re.compile( - r"NOT\s+READY|READY", re.IGNORECASE + r"\bNOT\s+READY\b|\bREADY\b", re.IGNORECASE ) diff --git a/lib/crewai/tests/utilities/test_structured_planning.py b/lib/crewai/tests/utilities/test_structured_planning.py index 92df3c0931..8f03737b0c 100644 --- a/lib/crewai/tests/utilities/test_structured_planning.py +++ b/lib/crewai/tests/utilities/test_structured_planning.py @@ -47,6 +47,17 @@ def test_case_insensitive(self): def test_no_marker_defaults_to_not_ready(self): assert _detect_plan_ready("just a plan with no verdict") is False + def test_ready_substring_inside_words_is_ignored(self): + # "already" contains "ready" but is not a READY marker (word boundaries). + assert _detect_plan_ready("I am already prepared") is False + assert _detect_plan_ready("The steps are already documented") is False + + def test_trailing_already_does_not_flip_not_ready(self): + # A trailing "already" must not override a concluding NOT READY. + assert ( + _detect_plan_ready("NOT READY. I already have the tools.") is False + ) + def test_parse_planning_response_detects_bare_ready(self): _plan, ready = AgentReasoning._parse_planning_response("my plan\n\nREADY") assert ready is True