Skip to content
15 changes: 11 additions & 4 deletions lib/crewai/src/crewai/agent/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1436,12 +1436,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
)
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

def _training_handler(self, task_prompt: str) -> str:
"""Handle training data for the agent task prompt to improve output on Training."""
Expand Down Expand Up @@ -2040,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
20 changes: 15 additions & 5 deletions lib/crewai/src/crewai/utilities/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def _coerce_response_to_pydantic(self, response: Any) -> BaseModel:
result=response,
model=self.model,
is_json_output=False,
agent=None,
agent=self.agent,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Async fallback blocks the event loop

High Severity

ato_pydantic still calls _coerce_response_to_pydantic, which is no longer I/O-free once agent is set. The instruction retry runs handle_partial_json → convert_with_instructions → to_pydantic → llm.call on the event loop, blocking async task export for the recovery path this change enables.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1aa889e. Configure here.

Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
)
if isinstance(partial, BaseModel):
return partial
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading