fix: support async before/after_kickoff_callbacks in akickoff#6482
fix: support async before/after_kickoff_callbacks in akickoff#6482magiccao wants to merge 8 commits into
Conversation
before_kickoff_callbacks and after_kickoff_callbacks were invoked synchronously in akickoff, silently dropping async callables and blocking the event loop on IO-bound sync callbacks. - Extract _begin_prepare_kickoff, _normalize_inputs, and _finish_prepare_kickoff helpers from prepare_kickoff to eliminate code duplication between the sync and async paths. - Add aprepare_kickoff (async counterpart of prepare_kickoff) that awaits coroutine before-callbacks via inspect.isawaitable, consistent with the existing task_callback pattern in task.py. - Use aprepare_kickoff in akickoff instead of prepare_kickoff. - Add inspect.isawaitable check for after_kickoff_callbacks in akickoff. - Add tests covering async before, async after, and mixed sync+async callback pipelines. kickoff_async is unaffected: it wraps the entire sync kickoff in asyncio.to_thread, so before/after callbacks already run off the event loop.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds async-aware handling for ChangesAsync kickoff callback support
Sequence Diagram(s)sequenceDiagram
participant Caller
participant Crew
participant aprepare_kickoff
participant BeforeCallback
participant AfterCallback
Caller->>Crew: akickoff(inputs)
Crew->>apprepare_kickoff: await preparation
aprepare_kickoff->>BeforeCallback: invoke callback
BeforeCallback-->>apprepare_kickoff: value or awaitable
aprepare_kickoff->>apprepare_kickoff: await awaitable result
aprepare_kickoff-->>Crew: prepared inputs
Crew->>Crew: execute tasks and build CrewOutput
Crew->>AfterCallback: invoke callback(CrewOutput)
AfterCallback-->>Crew: value or awaitable
Crew->>Crew: await awaitable result
Crew-->>Caller: return CrewOutput
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ErenAta16
left a comment
There was a problem hiding this comment.
Same fix as #6500 and #6494 (also open), and like #6500 this one refactors prepare_kickoff into shared helpers (_begin_prepare_kickoff/_normalize_inputs/_finish_prepare_kickoff) rather than duplicating the whole function body for the new async path, so prepare_kickoff and aprepare_kickoff stay in sync automatically as the shared logic evolves — same quality bar as #6500, just a different helper decomposition. Left the fuller three-way comparison on #6500's thread.
Also flagging the same thing I noted there: the description mentions IO-bound sync callbacks blocking the event loop as motivation, but the actual fix (here and in the other two) only adds the inspect.isawaitable check for async callables — a slow sync callback still runs inline. Worth confirming that is intentionally out of scope for this PR rather than an oversight, since the title/description read as covering both.
|
@ErenAta16 Thanks for the close read. To clarify the scope: the "blocking the event loop on IO-bound sync callbacks" motivation and the async-callable fix are actually the same root cause, not two separate concerns. In the old # Before — blocks event loop (only path available)
def before(inputs):
data = requests.get(...) # blocks
return {**inputs, **data}
# After — non-blocking (now actually supported)
async def before(inputs):
data = await httpx_client.get(...) # yields
return {**inputs, **data}So the blocking issue is resolved via the async path, not by offloading sync callbacks. The Re: the duplicate PRs (#6494, #6500) — happy to consolidate once maintainers indicate which approach they prefer; this one's helper decomposition ( |
|
That reframing holds up. The before/after example makes it clear: async callables were previously discarded outright (coroutine created, never awaited), so there was no non-blocking option at all, not even for callers willing to write No further concern on scope from me. Agreed consolidating with #6494/#6500 is a maintainer call once they weigh in on which decomposition they'd rather carry forward. |
…callbacks # Conflicts: # lib/crewai/src/crewai/crews/utils.py
…to fix/akickoff-async-callbacks
|
Gentle nudge — #6482, #6494, and #6500 all fix the same bug (async This PR:
Rebased on latest Happy to close in favor of whichever decomposition the team prefers — just let us know. An approve from a maintainer would unblock the merge. |
Summary
Fixes #6481
Crew.akickoff()is a native async execution path, butbefore_kickoff_callbacksandafter_kickoff_callbackswere invoked synchronously, silently discarding async callables and blocking the event loop on IO-bound sync callbacks.This PR brings
akickoffinto alignment with the existing async callback handling fortask_callbackandstep_callback.Changes
crews/utils.pyRefactors
prepare_kickoffby extracting three private helpers:_begin_prepare_kickoff— emission counter reset + resuming flag_normalize_inputs— input type validation and dict conversion_finish_prepare_kickoff— event emission, file handling, input interpolation, agent setup, planningAdds
aprepare_kickoff(async counterpart): identical toprepare_kickoffexcept thebefore_kickoff_callbacksloop usesinspect.isawaitable()+awaitto support coroutine callbacks.crew.pyakickoff: replacesprepare_kickoff(...)withawait aprepare_kickoff(...)akickoff: addsinspect.isawaitablecheck in theafter_kickoff_callbacksloopBehavior
akickoffkickoff_asyncNote on
kickoff_async: this PR only fixes async callback support inakickoff.kickoff_asyncrunskickoff()in a worker thread viaasyncio.to_thread, so sync callbacks won't block the event loop — but asyncbefore/after_kickoff_callbacksare still not awaited there. Fixingkickoff_asyncwould require a separate effort.Test plan
test_async_crew.pytests pass (sync callbacks unaffected)test_akickoff_calls_async_before_callbacks— async before callback is awaited; modified inputs reach task interpolationtest_akickoff_calls_async_after_callbacks— async after callback is awaited; result remainsCrewOutputtest_akickoff_mixed_sync_async_callbacks— mixed pipeline (sync_before → async_before → async_after → sync_after) executes in correct order and preserves the transform chain