Skip to content

Fix: Support async before/after_kickoff_callbacks in akickoff (#6481)#6547

Open
mukktinaadh wants to merge 1 commit into
crewAIInc:mainfrom
mukktinaadh:fix/async-callbacks-akickoff
Open

Fix: Support async before/after_kickoff_callbacks in akickoff (#6481)#6547
mukktinaadh wants to merge 1 commit into
crewAIInc:mainfrom
mukktinaadh:fix/async-callbacks-akickoff

Conversation

@mukktinaadh

Copy link
Copy Markdown

Closes #6481. Added aprepare_kickoff to properly await async before_kickoff_callbacks, and modified akickoff to await async after_kickoff_callbacks when using native async execution.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Async kickoff flow

Layer / File(s) Summary
Asynchronous kickoff preparation
lib/crewai/src/crewai/crews/utils.py
Adds aprepare_kickoff to asynchronously process inputs, before-kickoff callbacks, events, task context, agents, setup, and planning.
Crew async kickoff integration
lib/crewai/src/crewai/crew.py
Updates Crew.akickoff to await asynchronous preparation and conditionally await asynchronous after-kickoff callbacks.

Suggested reviewers: greysonlalonde

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: async kickoff callback support in akickoff.
Description check ✅ Passed The description directly matches the changeset by summarizing aprepare_kickoff and async callback handling in akickoff.
Linked Issues check ✅ Passed The changes implement async before_kickoff_callbacks and after_kickoff_callbacks support in akickoff, matching #6481's requirements.
Out of Scope Changes check ✅ Passed The summary shows only kickoff callback async support changes, with no unrelated code added.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Warning

⚠️ This pull request shows signs of AI-generated slop (defensive_cruft). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
lib/crewai/src/crewai/crews/utils.py (2)

529-529: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove redundant local import.

Mapping is already available in the module scope (as seen in prepare_kickoff), so this local import is unnecessary.

♻️ Proposed fix
-        from collections.abc import Mapping
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/crews/utils.py` at line 529, Remove the redundant local
Mapping import in the affected utility function, reusing the module-scope
Mapping import already used by prepare_kickoff; leave the surrounding logic
unchanged.

539-542: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Evaluate the result of the callback for awaitability instead of using iscoroutinefunction.

Checking if the callback itself is a coroutine function is fragile because it misses callables that return awaitables, such as class instances with an async __call__ method, functools.partial wrapped async functions, or mocked async functions in tests (e.g., AsyncMock). A more robust pattern is to execute the callback and then check if its returned result is awaitable. This approach directly aligns with the PR objective to "detect awaitable results".

  • lib/crewai/src/crewai/crews/utils.py#L539-L542: Execute before_callback(normalized), then use inspect.isawaitable() on the result to decide whether to await it.
  • lib/crewai/src/crewai/crew.py#L1258-L1263: Execute after_callback(result), then use inspect.isawaitable() on the result to decide whether to await it.
♻️ Proposed fixes

For lib/crewai/src/crewai/crews/utils.py:

-        if inspect.iscoroutinefunction(before_callback):
-            normalized = await before_callback(normalized)
-        else:
-            normalized = before_callback(normalized)
+        result = before_callback(normalized)
+        if inspect.isawaitable(result):
+            normalized = await result
+        else:
+            normalized = result

For lib/crewai/src/crewai/crew.py:

-            import inspect
-            for after_callback in self.after_kickoff_callbacks:
-                if inspect.iscoroutinefunction(after_callback):
-                    result = await after_callback(result)
-                else:
-                    result = after_callback(result)
+            import inspect
+            for after_callback in self.after_kickoff_callbacks:
+                callback_result = after_callback(result)
+                if inspect.isawaitable(callback_result):
+                    result = await callback_result
+                else:
+                    result = callback_result
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/crews/utils.py` around lines 539 - 542, Replace
coroutine-function checks with result-based awaitability detection: in
lib/crewai/src/crewai/crews/utils.py lines 539-542, execute
before_callback(normalized), then await the result only when
inspect.isawaitable() returns true; apply the same change to
after_callback(result) in lib/crewai/src/crewai/crew.py lines 1258-1263.
Preserve synchronous callback results unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/crewai/src/crewai/crews/utils.py`:
- Around line 600-601: Update async aprepare_kickoff to call the module-local
setup_agents function defined in this module instead of importing setup_agents
from crewai.agents.agent_builder.utilities.base_setup. Remove the imported
symbol and preserve the existing setup invocation behavior.
- Around line 561-566: Update the event emission flow around
crewai_event_bus.emit and future.result() so waiting for the concurrent future
does not block the async event loop. Await the future through an
asyncio-compatible mechanism while preserving the existing exception-swallowing
behavior.

---

Nitpick comments:
In `@lib/crewai/src/crewai/crews/utils.py`:
- Line 529: Remove the redundant local Mapping import in the affected utility
function, reusing the module-scope Mapping import already used by
prepare_kickoff; leave the surrounding logic unchanged.
- Around line 539-542: Replace coroutine-function checks with result-based
awaitability detection: in lib/crewai/src/crewai/crews/utils.py lines 539-542,
execute before_callback(normalized), then await the result only when
inspect.isawaitable() returns true; apply the same change to
after_callback(result) in lib/crewai/src/crewai/crew.py lines 1258-1263.
Preserve synchronous callback results unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 25b2b1c8-19da-4979-a52f-60970c3cda81

📥 Commits

Reviewing files that changed from the base of the PR and between 5f4ac9f and 19c0626.

📒 Files selected for processing (2)
  • lib/crewai/src/crewai/crew.py
  • lib/crewai/src/crewai/crews/utils.py

Comment on lines +561 to +566
future = crewai_event_bus.emit(crew, started_event)
if future is not None:
try:
future.result()
except Exception:
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Avoid blocking the event loop with future.result().

Calling .result() on a concurrent.futures.Future synchronously within an async def function blocks the asyncio event loop, potentially freezing other concurrent tasks. Yield control back to the event loop properly by awaiting the future.

⚡ Proposed fix
-        future = crewai_event_bus.emit(crew, started_event)
-        if future is not None:
-            try:
-                future.result()
-            except Exception:
-                pass
+        future = crewai_event_bus.emit(crew, started_event)
+        if future is not None:
+            try:
+                if inspect.isawaitable(future):
+                    await future
+                else:
+                    import asyncio
+                    await asyncio.wrap_future(future)
+            except Exception:
+                pass
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
future = crewai_event_bus.emit(crew, started_event)
if future is not None:
try:
future.result()
except Exception:
pass
future = crewai_event_bus.emit(crew, started_event)
if future is not None:
try:
if inspect.isawaitable(future):
await future
else:
import asyncio
await asyncio.wrap_future(future)
except Exception:
pass
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/crews/utils.py` around lines 561 - 566, Update the
event emission flow around crewai_event_bus.emit and future.result() so waiting
for the concurrent future does not block the async event loop. Await the future
through an asyncio-compatible mechanism while preserving the existing
exception-swallowing behavior.

Comment on lines +600 to +601
from crewai.agents.agent_builder.utilities.base_setup import setup_agents
setup_agents(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the local setup_agents function.

The async aprepare_kickoff imports setup_agents from crewai.agents.agent_builder.utilities.base_setup, whereas the synchronous prepare_kickoff uses the setup_agents function defined locally in this same module (at line 71). To maintain consistency and avoid potential import circularity or unexpected behavior differences, use the local function.

♻️ Proposed fix
-    from crewai.agents.agent_builder.utilities.base_setup import setup_agents
-    setup_agents(
+    setup_agents(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from crewai.agents.agent_builder.utilities.base_setup import setup_agents
setup_agents(
setup_agents(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/crews/utils.py` around lines 600 - 601, Update async
aprepare_kickoff to call the module-local setup_agents function defined in this
module instead of importing setup_agents from
crewai.agents.agent_builder.utilities.base_setup. Remove the imported symbol and
preserve the existing setup invocation behavior.

@ErenAta16 ErenAta16 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Checked this against the issue's own proposed fix pattern (and against how task_callback already handles this in the sync-task async path). One gap: both here and in aprepare_kickoff, the async-detection uses inspect.iscoroutinefunction(callback) checked before calling it, rather than inspect.isawaitable(result) checked after calling it (which is what the issue itself proposed, and what task_callback's existing code already does).

iscoroutinefunction only recognizes plain async def functions/methods. It returns False for a callable class instance with an async __call__, or for other general callables that return a coroutine without themselves being coroutine functions. SerializableCallable (the type alias before/after_kickoff_callbacks are typed as) is Callable[..., Any], so callable-class instances are a supported, type-checked use case here, not a hypothetical. For one of those, this fix's iscoroutinefunction check is False, so the callback is invoked synchronously, result becomes an unawaited coroutine, and it silently reproduces the exact original bug for that callback style.

Checking inspect.isawaitable(result) after the call instead (call first, then decide whether to await) would cover both plain async functions and callable-class instances, since it inspects the actual return value rather than trying to classify the callable in advance. Worth fixing before merge given the type hint explicitly allows the case this would miss.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] before/after_kickoff_callbacks do not support async callables in akickoff

2 participants