fix(crew): await async before/after_kickoff_callbacks in akickoff#6500
fix(crew): await async before/after_kickoff_callbacks in akickoff#6500nolanchic wants to merge 1 commit into
Conversation
akickoff invoked before/after_kickoff_callbacks without an awaitable check, so async callables were silently dropped: an async after-callback replaced the crew result with an unawaited coroutine, and async before-callbacks never ran because prepare_kickoff is synchronous. - Add aprepare_kickoff (async counterpart of prepare_kickoff) that awaits callbacks returning an awaitable, matching task/step_callback handling. Shared body extracted into _prepare_kickoff_impl to avoid duplication. - In akickoff, await the result of each after_kickoff_callback when it is awaitable. Sync kickoff and kickoff_async (to_thread wrapper) are unchanged. Fixes crewAIInc#6481
|
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 (3)
📝 WalkthroughWalkthroughThis PR adds async-aware handling for ChangesAsync kickoff callback support
Sequence Diagram(s)sequenceDiagram
participant Caller
participant Crew as Crew.akickoff()
participant Utils as aprepare_kickoff
participant BeforeCB as before_kickoff_callbacks
participant AfterCB as after_kickoff_callbacks
Caller->>Crew: akickoff(inputs)
Crew->>Utils: await aprepare_kickoff(inputs)
Utils->>BeforeCB: await callback(inputs)
BeforeCB-->>Utils: normalized inputs
Utils-->>Crew: prepared state
Crew->>Crew: execute tasks
Crew->>AfterCB: after_callback(result)
AfterCB-->>Crew: result (possibly coroutine)
Crew->>Crew: if isawaitable(result): await result
Crew-->>Caller: CrewOutput
Suggested reviewers: 🚥 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.
This, #6494, and #6482 are three open PRs fixing the same bug (async before/after_kickoff_callbacks silently discarded in akickoff) — the crew.py hunk is identical across all three (same inspect.isawaitable guard around the after-callback loop, same aprepare_kickoff swap-in). Compared the three crews/utils.py implementations, since that is where they actually diverge:
This one and #6482 both extract shared helpers so prepare_kickoff (sync) and aprepare_kickoff (async) share the common setup/teardown logic (event emission, input interpolation, agent setup, planning) rather than duplicating it — this one via a single _prepare_kickoff_impl taking an optional normalized_inputs override, #6482 via three smaller helpers (_begin_prepare_kickoff/_normalize_inputs/_finish_prepare_kickoff). #6494 instead copy-pastes the full ~80-line body of prepare_kickoff into a new aprepare_kickoff, which works but means the two functions can drift out of sync on a future change to the shared logic.
One honest scope note that applies equally to all three: the descriptions (this PR included) mention IO-bound sync callbacks blocking the event loop as part of the motivation, but none of the three actually route sync callbacks through run_in_executor or similar — they only add the inspect.isawaitable check, so a slow sync callback still runs inline on the event loop same as before. Not a defect in this PR specifically, just flagging that the fix (correctly) narrows to "stop silently dropping async callables" rather than also solving the blocking-event-loop half of the stated problem.
Description
Crew.akickoff()is a native async path, butbefore_kickoff_callbacksandafter_kickoff_callbackswere invoked without any awaitable check — silently dropping async callables:after_kickoff_callbacks:result = after_callback(result)returned an unawaited coroutine when the callback wasasync, so the crew result was silently replaced with a coroutine object (never executed, never awaited).before_kickoff_callbacks: invoked inside the synchronousprepare_kickoff(), which has no async support — async before-callbacks never ran, and any blocking I/O inside a sync before-callback stalled the event loop.This is inconsistent with
task_callbackandstep_callback, which correctly checkinspect.isawaitablein the async path.Changes
crews/utils.py: addasync def aprepare_kickoff(...)— the async counterpart ofprepare_kickoff. It awaits before-callbacks that return an awaitable (sync callbacks run unchanged). The shared body is extracted into_prepare_kickoff_impl(...)so the two entry points stay DRY and behavior is identical apart from callback awaiting.crew.py:akickoffnow callsawait aprepare_kickoff(...)instead ofprepare_kickoff(...), and awaits each after-callback result when it is awaitable.Behavior preserved
kickoff()(sync) — unchanged, still usesprepare_kickoff.kickoff_async()— unchanged; it wraps the synckickoffinasyncio.to_thread, so all callbacks already run off the event loop.akickoff— unchanged (anisawaitablecheck is a no-op for them).Test plan
test_akickoff_awaits_async_after_callbacks— async after-callback is awaited; result stays aCrewOutput(was a coroutine before).test_akickoff_applies_async_after_callback_result— async after-callback's returnedCrewOutputreplaces the result.test_akickoff_awaits_async_before_callbacks— async before-callback is awaited and runs.test_akickoff_applies_async_before_callback_inputs— async before-callback's returned inputs flow into the crew.uv run pytest lib/crewai/tests/crew/test_async_crew.py lib/crewai/tests/test_crew.py lib/crewai/tests/test_checkpoint.py→ all pass (sync kickoff + kickoff_async callbacks unaffected).uv run ruff check lib/→ clean.uv run mypy lib/crewai/src/crewai/crew.py lib/crewai/src/crewai/crews/utils.py→ clean (strict).Fixes #6481.