@@ -1803,6 +1803,42 @@ def _finalize_kickoff(
18031803
18041804 return output
18051805
1806+ async def _finalize_kickoff_async (
1807+ self ,
1808+ output : LiteAgentOutput ,
1809+ executor : AgentExecutor ,
1810+ inputs : dict [str , str ],
1811+ response_format : type [Any ] | None ,
1812+ messages : str | list [LLMMessage ],
1813+ agent_info : dict [str , Any ],
1814+ usage_baseline : UsageMetrics | None = None ,
1815+ ) -> LiteAgentOutput :
1816+ """Async variant of _finalize_kickoff for kickoff_async.
1817+
1818+ Identical except guardrail retries stay on the async execution path;
1819+ see _process_kickoff_guardrail_async.
1820+ """
1821+ if self .guardrail is not None :
1822+ output = await self ._process_kickoff_guardrail_async (
1823+ output = output ,
1824+ executor = executor ,
1825+ inputs = inputs ,
1826+ response_format = response_format ,
1827+ usage_baseline = usage_baseline ,
1828+ )
1829+
1830+ self ._save_kickoff_to_memory (messages , output .raw )
1831+
1832+ crewai_event_bus .emit (
1833+ self ,
1834+ event = LiteAgentExecutionCompletedEvent (
1835+ agent_info = agent_info ,
1836+ output = output .raw ,
1837+ ),
1838+ )
1839+
1840+ return output
1841+
18061842 def _emit_kickoff_error (self , agent_info : dict [str , Any ], e : Exception ) -> NoReturn :
18071843 """Emit a kickoff error event and re-raise."""
18081844 crewai_event_bus .emit (
@@ -1972,6 +2008,31 @@ async def _execute_and_build_output_async(
19722008 result , executor , response_format , usage_baseline , kickoff_failures
19732009 )
19742010
2011+ def _resolve_guardrail_callable (self ) -> GuardrailCallable | None :
2012+ """Return the configured guardrail as a callable, or None if unset."""
2013+ if isinstance (self .guardrail , str ):
2014+ from crewai .tasks .llm_guardrail import LLMGuardrail
2015+
2016+ return cast (
2017+ GuardrailCallable ,
2018+ LLMGuardrail (description = self .guardrail , llm = cast (BaseLLM , self .llm )),
2019+ )
2020+ if callable (self .guardrail ):
2021+ return self .guardrail
2022+ return None
2023+
2024+ @staticmethod
2025+ def _apply_guardrail_result (
2026+ output : LiteAgentOutput , guardrail_result : Any
2027+ ) -> LiteAgentOutput :
2028+ """Fold an accepted guardrail result into the output (shared sync/async)."""
2029+ if guardrail_result .result is not None :
2030+ if isinstance (guardrail_result .result , str ):
2031+ output .raw = guardrail_result .result
2032+ elif isinstance (guardrail_result .result , BaseModel ):
2033+ output .pydantic = guardrail_result .result
2034+ return output
2035+
19752036 def _process_kickoff_guardrail (
19762037 self ,
19772038 output : LiteAgentOutput ,
@@ -1996,17 +2057,8 @@ def _process_kickoff_guardrail(
19962057 Returns:
19972058 Validated/updated output.
19982059 """
1999- guardrail_callable : GuardrailCallable
2000- if isinstance (self .guardrail , str ):
2001- from crewai .tasks .llm_guardrail import LLMGuardrail
2002-
2003- guardrail_callable = cast (
2004- GuardrailCallable ,
2005- LLMGuardrail (description = self .guardrail , llm = cast (BaseLLM , self .llm )),
2006- )
2007- elif callable (self .guardrail ):
2008- guardrail_callable = self .guardrail
2009- else :
2060+ guardrail_callable = self ._resolve_guardrail_callable ()
2061+ if guardrail_callable is None :
20102062 return output
20112063
20122064 guardrail_result = process_guardrail (
@@ -2048,13 +2100,68 @@ def _process_kickoff_guardrail(
20482100 usage_baseline = usage_baseline ,
20492101 )
20502102
2051- if guardrail_result .result is not None :
2052- if isinstance (guardrail_result .result , str ):
2053- output .raw = guardrail_result .result
2054- elif isinstance (guardrail_result .result , BaseModel ):
2055- output .pydantic = guardrail_result .result
2103+ return self ._apply_guardrail_result (output , guardrail_result )
20562104
2057- return output
2105+ async def _process_kickoff_guardrail_async (
2106+ self ,
2107+ output : LiteAgentOutput ,
2108+ executor : AgentExecutor ,
2109+ inputs : dict [str , str ],
2110+ response_format : type [Any ] | None = None ,
2111+ retry_count : int = 0 ,
2112+ usage_baseline : UsageMetrics | None = None ,
2113+ ) -> LiteAgentOutput :
2114+ """Async variant of _process_kickoff_guardrail for kickoff_async.
2115+
2116+ Identical except the retry re-executes through
2117+ _execute_and_build_output_async: the sync executor.invoke() detects a
2118+ running event loop and hands back an unawaited coroutine instead of a
2119+ result dict, which then crashes in _build_output_from_result.
2120+ """
2121+ guardrail_callable = self ._resolve_guardrail_callable ()
2122+ if guardrail_callable is None :
2123+ return output
2124+
2125+ guardrail_result = process_guardrail (
2126+ output = output ,
2127+ guardrail = guardrail_callable ,
2128+ retry_count = retry_count ,
2129+ event_source = self ,
2130+ from_agent = self ,
2131+ )
2132+
2133+ if not guardrail_result .success :
2134+ if retry_count >= self .guardrail_max_retries :
2135+ raise ValueError (
2136+ f"Agent's guardrail failed validation after { self .guardrail_max_retries } retries. "
2137+ f"Last error: { guardrail_result .error } "
2138+ )
2139+
2140+ executor ._append_message_to_state (
2141+ guardrail_result .error or "Guardrail validation failed" ,
2142+ role = "user" ,
2143+ )
2144+
2145+ retried = await self ._execute_and_build_output_async (
2146+ executor , inputs , response_format , usage_baseline
2147+ )
2148+ # The retry opens its own collector, so carry the blocked attempt's
2149+ # failures forward or they vanish from the final output.
2150+ retried .tool_failures = merge_tool_failures (
2151+ output .tool_failures , retried .tool_failures
2152+ )
2153+ output = retried
2154+
2155+ return await self ._process_kickoff_guardrail_async (
2156+ output = output ,
2157+ executor = executor ,
2158+ inputs = inputs ,
2159+ response_format = response_format ,
2160+ retry_count = retry_count + 1 ,
2161+ usage_baseline = usage_baseline ,
2162+ )
2163+
2164+ return self ._apply_guardrail_result (output , guardrail_result )
20582165
20592166 async def kickoff_async (
20602167 self ,
@@ -2118,7 +2225,7 @@ async def kickoff_async(
21182225 output = await self ._execute_and_build_output_async (
21192226 executor , inputs , response_format , usage_baseline
21202227 )
2121- return self ._finalize_kickoff (
2228+ return await self ._finalize_kickoff_async (
21222229 output ,
21232230 executor ,
21242231 inputs ,
0 commit comments