From 1aa889e15ca5a93333d78dde2c43b88459ecc78d Mon Sep 17 00:00:00 2001 From: TINGyu123644 Date: Sun, 18 Oct 2026 14:47:41 +0800 Subject: [PATCH 1/3] fix(converter): preserve agent reference in pydantic coercion fallback When Pydantic validation fails and _coerce_response_to_pydantic falls back to handle_partial_json, the agent reference was hardcoded to None. If the partial-JSON path then needs another LLM round-trip (instruction fallback), it crashes because handle_partial_json expects an agent with an llm. - Add optional gent: Any = None field to OutputConverter base. - Pass self.agent instead of None in _coerce_response_to_pydantic. - Make Agent.get_output_converter an instance method and pass agent=self. This is a rebase of #7989 onto current main after the monorepo restructure. --- lib/crewai/src/crewai/agent/core.py | 7 ++++--- .../agent_builder/utilities/base_output_converter.py | 5 +++++ lib/crewai/src/crewai/utilities/converter.py | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/crewai/src/crewai/agent/core.py b/lib/crewai/src/crewai/agent/core.py index a24ca152a5..6ebe3fd82e 100644 --- a/lib/crewai/src/crewai/agent/core.py +++ b/lib/crewai/src/crewai/agent/core.py @@ -1426,12 +1426,13 @@ def get_code_execution_tools(self) -> list[Any]: ) return [] - @staticmethod def get_output_converter( - llm: BaseLLM, text: str, model: type[BaseModel], instructions: str + self, llm: BaseLLM, text: str, model: type[BaseModel], instructions: str ) -> Converter: """Create a Converter instance for transforming LLM output to a structured model.""" - return Converter(llm=llm, text=text, model=model, instructions=instructions) + return Converter( + llm=llm, text=text, model=model, instructions=instructions, agent=self + ) def _training_handler(self, task_prompt: str) -> str: """Handle training data for the agent task prompt to improve output on Training.""" diff --git a/lib/crewai/src/crewai/agents/agent_builder/utilities/base_output_converter.py b/lib/crewai/src/crewai/agents/agent_builder/utilities/base_output_converter.py index b8709ef743..a46f8e73de 100644 --- a/lib/crewai/src/crewai/agents/agent_builder/utilities/base_output_converter.py +++ b/lib/crewai/src/crewai/agents/agent_builder/utilities/base_output_converter.py @@ -32,6 +32,11 @@ class OutputConverter(BaseModel, ABC): description="Max number of attempts to try to get the output formatted.", default=3, ) + agent: Any = Field( + default=None, + description="Optional reference to the calling agent, used when the " + "partial-JSON fallback needs to run another LLM round-trip.", + ) @abstractmethod def to_pydantic(self, current_attempt: int = 1) -> BaseModel: diff --git a/lib/crewai/src/crewai/utilities/converter.py b/lib/crewai/src/crewai/utilities/converter.py index f55888589d..aa2978c572 100644 --- a/lib/crewai/src/crewai/utilities/converter.py +++ b/lib/crewai/src/crewai/utilities/converter.py @@ -64,7 +64,7 @@ def _coerce_response_to_pydantic(self, response: Any) -> BaseModel: result=response, model=self.model, is_json_output=False, - agent=None, + agent=self.agent, ) if isinstance(partial, BaseModel): return partial From ed1af629c4241c50c4a91f58f5bb0ba4674c3b0a Mon Sep 17 00:00:00 2001 From: TINGyu123644 <2663180454@qq.com> Date: Sat, 31 Oct 2026 20:08:28 +0800 Subject: [PATCH 2/3] fix(converter): keep instruction-refit converters agent-free cursor[bot] flagged that stamping the calling agent onto the Converter built inside convert_with_instructions lets a failed refit route its own partial-JSON fallback back through convert_with_instructions, spawning a nested LLM call tree on every failed attempt. The refit converter is now built with agent=None (falling back to the base Converter when no custom converter_cls is given), so a ValidationError stays inside that converter's max_attempts loop. The top-level agent stamping that fixes #7984 is unchanged: the refit still runs with the agent's LLM and still recovers off-schema output. Empirically verified with a stubbed-LLM harness: always-invalid output now costs 12 provider calls (measured 183 before this change); refit recovery and refit self-retry paths still pass. --- lib/crewai/src/crewai/utilities/converter.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/crewai/src/crewai/utilities/converter.py b/lib/crewai/src/crewai/utilities/converter.py index d8d87bbaa9..5f269187b6 100644 --- a/lib/crewai/src/crewai/utilities/converter.py +++ b/lib/crewai/src/crewai/utilities/converter.py @@ -405,9 +405,14 @@ def convert_with_instructions( raise ValueError("Agent must have a valid LLM instance for conversion") instructions = get_conversion_instructions(model=model, llm=llm) + # The refit converter must NOT carry the calling agent: an agent-stamped + # converter routes its own partial-JSON fallback back through + # convert_with_instructions, spawning a nested LLM call tree on every + # failed attempt (unbounded provider fan-out). With agent=None, a + # ValidationError stays inside this converter's max_attempts loop. converter = create_converter( - agent=agent, - converter_cls=converter_cls, + agent=None, + converter_cls=converter_cls or Converter, llm=llm, text=result, model=model, @@ -549,9 +554,14 @@ async def async_convert_with_instructions( raise ValueError("Agent must have a valid LLM instance for conversion") instructions = get_conversion_instructions(model=model, llm=llm) + # The refit converter must NOT carry the calling agent: an agent-stamped + # converter routes its own partial-JSON fallback back through + # convert_with_instructions, spawning a nested LLM call tree on every + # failed attempt (unbounded provider fan-out). With agent=None, a + # ValidationError stays inside this converter's max_attempts loop. converter = create_converter( - agent=agent, - converter_cls=converter_cls, + agent=None, + converter_cls=converter_cls or Converter, llm=llm, text=result, model=model, From 07116d482a85d53fc6384905218be08aaeddf423 Mon Sep 17 00:00:00 2001 From: TINGyu123644 <2663180454@qq.com> Date: Sun, 1 Nov 2026 23:46:52 +0800 Subject: [PATCH 3/3] fix(converter): carry agent into kickoff fallback converter The response_format fallback in Agent._build_lite_agent_output built a Converter without the agent, so its partial-JSON path entered convert_with_instructions(agent=None) and raised the #7984 TypeError on every attempt - the designed instructions-based refit never ran. Build it through self.get_output_converter() so the fallback converter carries the agent. The refit converter created inside convert_with_instructions is still agent-less, so the nested LLM call tree found earlier cannot reappear here. Verified with a stubbed-LLM harness: - agent-less construction (before): 3 provider calls, TypeError inside the ConverterError chain, no recovery; - agent-stamped construction (after): recovery on the instructed refit round (2 calls); - always-failing LLM (after): bounded at 12 provider calls. --- lib/crewai/src/crewai/agent/core.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/crewai/src/crewai/agent/core.py b/lib/crewai/src/crewai/agent/core.py index 77ee231abc..966c58d027 100644 --- a/lib/crewai/src/crewai/agent/core.py +++ b/lib/crewai/src/crewai/agent/core.py @@ -2041,7 +2041,13 @@ def _build_output_from_result( "formatted_task_instructions" ).format(output_format=schema) - converter = Converter( + # Build the fallback converter through get_output_converter + # so it carries the agent: the partial-JSON path can then + # run the (bounded) instructions-based refit instead of + # aborting with the #7984 TypeError. The refit converter + # itself is still agent-less (see convert_with_instructions), + # so no nested LLM call tree can form here either. + converter = self.get_output_converter( llm=cast(BaseLLM, self.llm), text=raw_output, model=response_format,