Skip to content

fix: use async LLM calls in converter for async workflows (issue #5230)#5231

Open
devin-ai-integration[bot] wants to merge 3 commits intomainfrom
devin/1775133051-fix-async-converter-blocking
Open

fix: use async LLM calls in converter for async workflows (issue #5230)#5231
devin-ai-integration[bot] wants to merge 3 commits intomainfrom
devin/1775133051-fix-async-converter-blocking

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot commented Apr 2, 2026

Summary

When using akickoff() (async workflow), the converter module was called synchronously via self._export_output() inside _aexecute_core and _ainvoke_guardrail_function, which calls llm.call() and blocks the event loop.

This PR adds async counterparts throughout the converter pipeline:

  • Converter.ato_pydantic() / Converter.ato_json() — use llm.acall() instead of llm.call()
  • aconvert_to_model(), ahandle_partial_json(), aconvert_with_instructions() — async versions of the module-level utility functions
  • Task._aexport_output() — async version of _export_output wired into _aexecute_core (line 605) and both branches of _ainvoke_guardrail_function (lines 1286, 1331)

Sync methods are untouched; backward compatibility is preserved.

Review & Testing Checklist for Human

  • ato_json still calls sync _create_instructor().to_json() when supports_function_calling() is True — this means the function-calling JSON path still blocks the event loop. The instructor library doesn't expose an async interface here. Verify this is acceptable or needs a run_in_executor wrapper.
  • Verify all three _aexport_output call sites in task.py are correct (lines ~605, ~1286, ~1331). Confirm no other async paths still call the sync _export_output.
  • ato_pydantic non-function-calling fallback calls sync handle_partial_json on ValidationError — this is safe (no LLM call, just regex/JSON parsing) but worth confirming.
  • Run an end-to-end async workflow with output_pydantic or output_json set on a task, using crew.akickoff(), and confirm the event loop is no longer blocked during output conversion.

Notes

  • All new async utility functions mirror the logic of their sync counterparts. The duplication is intentional to avoid changing the sync code path.
  • 20 new async tests added covering: ato_pydantic/ato_json happy paths, retry logic, max-attempt errors, all three async utility functions, partial JSON handling, and explicit assertions that llm.call() is never invoked from the async path.

Link to Devin session: https://app.devin.ai/sessions/fec9a6f7345348bd99ecbc42d20e7876


Note

Medium Risk
Updates async task execution to use new async conversion helpers, which changes how structured outputs are parsed and may affect task results/guardrail retries in async workflows. Risk is contained to async paths; sync behavior is preserved.

Overview
Fixes async workflows (Task._aexecute_core / _ainvoke_guardrail_function) so structured output export no longer calls the synchronous converter and blocks the event loop.

Adds an async converter pipeline in utilities/converter.py (Converter.ato_pydantic/ato_json plus aconvert_to_model/ahandle_partial_json/aconvert_with_instructions) and wires it via a new Task._aexport_output, with expanded async test coverage ensuring llm.call() is never used on the async path.

Written by Cursor Bugbot for commit 415b894. This will update automatically on new commits. Configure here.

When using akickoff() (async workflow), the _export_output method and
_ainvoke_guardrail_function were calling synchronous llm.call() through
the converter module, which blocks the event loop.

Changes:
- Add ato_pydantic() and ato_json() async methods to Converter class
  that use llm.acall() instead of llm.call()
- Add async utility functions: aconvert_to_model(), ahandle_partial_json(),
  aconvert_with_instructions()
- Add _aexport_output() async method to Task class
- Wire _aexport_output into _aexecute_core and _ainvoke_guardrail_function
  replacing the sync _export_output calls in async paths
- Add comprehensive async tests covering all new async converter paths

Fixes #5230

Co-Authored-By: João <joao@crewai.com>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

Prompt hidden (unlisted session)

@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

devin-ai-integration bot and others added 2 commits April 2, 2026 12:44
Co-Authored-By: João <joao@crewai.com>
Addresses Cursor Bugbot feedback: the function-calling path in ato_json
was still calling _create_instructor().to_json() synchronously, blocking
the event loop. Now wrapped in asyncio.to_thread() to offload the
blocking call to a separate thread.

Co-Authored-By: João <joao@crewai.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants