Description
When an Agent is executed with kickoff_async() and its guardrail rejects the
first output, CrewAI attempts the guardrail retry through the synchronous
executor path.
Because the retry is still running inside an event loop, AgentExecutor.invoke()
returns the invoke_async() coroutine instead of a result dictionary. CrewAI
then passes that coroutine to _build_output_from_result(), which calls
result.get("output", "").
This raises:
AttributeError: 'coroutine' object has no attribute 'get'
RuntimeWarning: coroutine 'AgentExecutor.invoke_async' was never awaited
As a result, guardrail_max_retries does not work for kickoff_async(): the
first rejected output crashes before the retry can complete.
Steps to Reproduce
- Install
crewai==1.15.18.
- Configure a valid LLM provider/API key.
- Run the following script:
import asyncio
from crewai import Agent
def always_reject(_result):
return False, "Force a guardrail retry."
async def main():
agent = Agent(
role="Test agent",
goal="Return a short answer.",
backstory="Minimal reproduction for an async guardrail retry.",
llm="openai/gpt-4o-mini",
guardrail=always_reject,
guardrail_max_retries=1,
verbose=False,
)
await agent.kickoff_async(
messages=[{"role": "user", "content": "Say hello."}],
)
if __name__ == "__main__":
asyncio.run(main())
- Wait for the first agent response to be rejected by
always_reject.
- Observe that the retry crashes with
AttributeError instead of executing
asynchronously.
Expected behavior
After a guardrail rejects output produced by Agent.kickoff_async(), CrewAI
should execute the retry through the asynchronous executor path, await it, and
then evaluate the guardrail again.
With guardrail_max_retries=1, the agent should either:
- Return a valid second output if it passes the guardrail; or
- Raise the normal guardrail-validation error after the configured retry limit.
It should not attempt to use a coroutine as a dictionary.
Screenshots/Code snippets
Relevant execution path in crewai==1.15.18:
-
Agent.kickoff_async() executes the initial run asynchronously:
output = await self._execute_and_build_output_async(
executor, inputs, response_format, usage_baseline
)
return self._finalize_kickoff(...)
-
_finalize_kickoff() invokes the synchronous guardrail handler:
output = self._process_kickoff_guardrail(...)
-
When the guardrail fails, _process_kickoff_guardrail() retries through
the synchronous method:
retried = self._execute_and_build_output(
executor, inputs, response_format, usage_baseline
)
-
That method calls:
result = executor.invoke(inputs)
-
However, AgentExecutor.invoke() explicitly returns self.invoke_async(inputs)
when it detects an existing event loop:
if is_inside_event_loop():
return self.invoke_async(inputs)
The returned coroutine is then treated as a dictionary by
_build_output_from_result():
output = result.get("output", "")
Resulting traceback excerpt:
File ".../crewai/agent/core.py", line 2008, in _process_kickoff_guardrail
retried = self._execute_and_build_output(...)
File ".../crewai/agent/core.py", line 1933, in _execute_and_build_output
return self._build_output_from_result(...)
File ".../crewai/agent/core.py", line 1856, in _build_output_from_result
output = result.get("output", "")
AttributeError: 'coroutine' object has no attribute 'get'
RuntimeWarning: coroutine 'AgentExecutor.invoke_async' was never awaited
Operating System
Ubuntu 22.04
Python Version
3.10
crewAI Version
1.15.18
crewAI Tools Version
1.15.18
Virtual Environment
Venv
Evidence
This occurred in a production-style Flow where a synchronous custom guardrail
validated that the final answer cited available internal evidence.
The first guardrail rejection worked as expected.
The failure happened only when CrewAI tried to retry the agent after that
rejection:
AttributeError: 'coroutine' object has no attribute 'get'
RuntimeWarning: coroutine 'AgentExecutor.invoke_async' was never awaited
The issue is independent of the guardrail's validation rule: any guardrail that
returns (False, "...") after kickoff_async() should exercise the same path.
Traceback (most recent call last):
File "/home/jucelio-quentino/projects/internal-research/.venv/bin/kickoff", line 10, in <module>
sys.exit(kickoff())
File "/home/jucelio-quentino/projects/internal-research/cli/kickoff.py", line 9, in kickoff
asyncio.run(ChatSession().run())
File "/usr/lib/python3.10/asyncio/runners.py", line 44, in run
return loop.run_until_complete(main)
File "/usr/lib/python3.10/asyncio/base_events.py", line 649, in run_until_complete
return future.result()
File "/home/jucelio-quentino/projects/internal-research/cli/chat_session.py", line 41, in run
report = await self._run_flow(user_input)
File "/home/jucelio-quentino/projects/internal-research/cli/chat_session.py", line 72, in _run_flow
result = await flow.akickoff()
File "/home/jucelio-quentino/projects/internal-research/.venv/lib/python3.10/site-packages/crewai/flow/runtime/__init__.py", line 2757, in akickoff
return await self.kickoff_async(
File "/home/jucelio-quentino/projects/internal-research/src/internal_research/main.py", line 41, in kickoff_async
result = await super().kickoff_async(
File "/home/jucelio-quentino/projects/internal-research/.venv/lib/python3.10/site-packages/crewai/flow/runtime/__init__.py", line 2390, in kickoff_async
await asyncio.gather(*tasks)
File "/home/jucelio-quentino/projects/internal-research/.venv/lib/python3.10/site-packages/crewai/flow/runtime/__init__.py", line 2833, in _execute_start_method
result, finished_event_id = await self._execute_method(
File "/home/jucelio-quentino/projects/internal-research/.venv/lib/python3.10/site-packages/crewai/flow/runtime/__init__.py", line 3071, in _execute_method
raise e
File "/home/jucelio-quentino/projects/internal-research/.venv/lib/python3.10/site-packages/crewai/flow/runtime/__init__.py", line 2967, in _execute_method
result = await method(*args, **kwargs)
File "/home/jucelio-quentino/projects/internal-research/.venv/lib/python3.10/site-packages/crewai/flow/runtime/__init__.py", line 2880, in enhanced_method
return await original_method(*args, **kwargs)
File "/home/jucelio-quentino/projects/internal-research/src/internal_research/main.py", line 79, in research
answer_markdown = await researcher.kickoff(
File "/home/jucelio-quentino/projects/internal-research/src/internal_research/solo_researcher.py", line 134, in kickoff
output = await self.agent().kickoff_async(messages=messages)
File "/home/jucelio-quentino/projects/internal-research/.venv/lib/python3.10/site-packages/crewai/agent/core.py", line 2108, in kickoff_async
self._emit_kickoff_error(agent_info, e)
File "/home/jucelio-quentino/projects/internal-research/.venv/lib/python3.10/site-packages/crewai/agent/core.py", line 1793, in _emit_kickoff_error
raise e
File "/home/jucelio-quentino/projects/internal-research/.venv/lib/python3.10/site-packages/crewai/agent/core.py", line 2097, in kickoff_async
return self._finalize_kickoff(
File "/home/jucelio-quentino/projects/internal-research/.venv/lib/python3.10/site-packages/crewai/agent/core.py", line 1764, in _finalize_kickoff
output = self._process_kickoff_guardrail(
File "/home/jucelio-quentino/projects/internal-research/.venv/lib/python3.10/site-packages/crewai/agent/core.py", line 2008, in _process_kickoff_guardrail
retried = self._execute_and_build_output(
File "/home/jucelio-quentino/projects/internal-research/.venv/lib/python3.10/site-packages/crewai/agent/core.py", line 1933, in _execute_and_build_output
return self._build_output_from_result(
File "/home/jucelio-quentino/projects/internal-research/.venv/lib/python3.10/site-packages/crewai/agent/core.py", line 1856, in _build_output_from_result
output = result.get("output", "")
AttributeError: 'coroutine' object has no attribute 'get'
sys:1: RuntimeWarning: coroutine 'AgentExecutor.invoke_async' was never awaited
Possible Solution
Introduce an async guardrail-finalization/retry path for kickoff_async().
For example, kickoff_async() should call an async equivalent of
_finalize_kickoff() / _process_kickoff_guardrail() that retries with:
await self._execute_and_build_output_async(...)
instead of calling _execute_and_build_output(...).
Alternatively, make _process_kickoff_guardrail() detect whether
executor.invoke(inputs) returned an awaitable and await it before building the
output. A dedicated async implementation would be clearer and avoid mixing the
sync and async execution paths.
Additional context
The guardrail is configured directly on the Agent with
guardrail_max_retries=2. The first guardrail evaluation is emitted correctly,
but the retry fails before a second evaluation occurs.
I have not tested a newer CrewAI version yet. The reproduction and source-path
analysis above are from the installed 1.15.18 release.
Description
When an
Agentis executed withkickoff_async()and its guardrail rejects thefirst output, CrewAI attempts the guardrail retry through the synchronous
executor path.
Because the retry is still running inside an event loop,
AgentExecutor.invoke()returns the
invoke_async()coroutine instead of a result dictionary. CrewAIthen passes that coroutine to
_build_output_from_result(), which callsresult.get("output", "").This raises:
As a result,
guardrail_max_retriesdoes not work forkickoff_async(): thefirst rejected output crashes before the retry can complete.
Steps to Reproduce
crewai==1.15.18.always_reject.AttributeErrorinstead of executingasynchronously.
Expected behavior
After a guardrail rejects output produced by
Agent.kickoff_async(), CrewAIshould execute the retry through the asynchronous executor path, await it, and
then evaluate the guardrail again.
With
guardrail_max_retries=1, the agent should either:It should not attempt to use a coroutine as a dictionary.
Screenshots/Code snippets
Relevant execution path in
crewai==1.15.18:Agent.kickoff_async()executes the initial run asynchronously:_finalize_kickoff()invokes the synchronous guardrail handler:When the guardrail fails,
_process_kickoff_guardrail()retries throughthe synchronous method:
That method calls:
However,
AgentExecutor.invoke()explicitly returnsself.invoke_async(inputs)when it detects an existing event loop:
The returned coroutine is then treated as a dictionary by
_build_output_from_result():Resulting traceback excerpt:
Operating System
Ubuntu 22.04
Python Version
3.10
crewAI Version
1.15.18
crewAI Tools Version
1.15.18
Virtual Environment
Venv
Evidence
This occurred in a production-style Flow where a synchronous custom guardrail
validated that the final answer cited available internal evidence.
The first guardrail rejection worked as expected.
The failure happened only when CrewAI tried to retry the agent after that
rejection:
The issue is independent of the guardrail's validation rule: any guardrail that
returns
(False, "...")afterkickoff_async()should exercise the same path.Possible Solution
Introduce an async guardrail-finalization/retry path for
kickoff_async().For example,
kickoff_async()should call an async equivalent of_finalize_kickoff()/_process_kickoff_guardrail()that retries with:instead of calling
_execute_and_build_output(...).Alternatively, make
_process_kickoff_guardrail()detect whetherexecutor.invoke(inputs)returned an awaitable and await it before building theoutput. A dedicated async implementation would be clearer and avoid mixing the
sync and async execution paths.
Additional context
The guardrail is configured directly on the
Agentwithguardrail_max_retries=2. The first guardrail evaluation is emitted correctly,but the retry fails before a second evaluation occurs.
I have not tested a newer CrewAI version yet. The reproduction and source-path
analysis above are from the installed
1.15.18release.