diff --git a/src/crewai/utilities/events/event_listener.py b/src/crewai/utilities/events/event_listener.py index a76b87964..caf83bfdc 100644 --- a/src/crewai/utilities/events/event_listener.py +++ b/src/crewai/utilities/events/event_listener.py @@ -56,6 +56,11 @@ from .tool_usage_events import ( ToolUsageFinishedEvent, ToolUsageStartedEvent, ) +from .reasoning_events import ( + AgentReasoningStartedEvent, + AgentReasoningCompletedEvent, + AgentReasoningFailedEvent, +) class EventListener(BaseEventListener): @@ -406,5 +411,30 @@ class EventListener(BaseEventListener): self.formatter.current_crew_tree, ) + # ----------- REASONING EVENTS ----------- + + @crewai_event_bus.on(AgentReasoningStartedEvent) + def on_agent_reasoning_started(source, event: AgentReasoningStartedEvent): + self.formatter.handle_reasoning_started( + self.formatter.current_agent_branch, + event.attempt, + self.formatter.current_crew_tree, + ) + + @crewai_event_bus.on(AgentReasoningCompletedEvent) + def on_agent_reasoning_completed(source, event: AgentReasoningCompletedEvent): + self.formatter.handle_reasoning_completed( + event.plan, + event.ready, + self.formatter.current_crew_tree, + ) + + @crewai_event_bus.on(AgentReasoningFailedEvent) + def on_agent_reasoning_failed(source, event: AgentReasoningFailedEvent): + self.formatter.handle_reasoning_failed( + event.error, + self.formatter.current_crew_tree, + ) + event_listener = EventListener() diff --git a/src/crewai/utilities/events/event_types.py b/src/crewai/utilities/events/event_types.py index 4e0673758..5d4a41dcc 100644 --- a/src/crewai/utilities/events/event_types.py +++ b/src/crewai/utilities/events/event_types.py @@ -43,6 +43,11 @@ from .tool_usage_events import ( ToolUsageFinishedEvent, ToolUsageStartedEvent, ) +from .reasoning_events import ( + AgentReasoningStartedEvent, + AgentReasoningCompletedEvent, + AgentReasoningFailedEvent, +) EventTypes = Union[ CrewKickoffStartedEvent, @@ -74,4 +79,7 @@ EventTypes = Union[ LLMStreamChunkEvent, LLMGuardrailStartedEvent, LLMGuardrailCompletedEvent, + AgentReasoningStartedEvent, + AgentReasoningCompletedEvent, + AgentReasoningFailedEvent, ] diff --git a/src/crewai/utilities/events/reasoning_events.py b/src/crewai/utilities/events/reasoning_events.py new file mode 100644 index 000000000..03d484de3 --- /dev/null +++ b/src/crewai/utilities/events/reasoning_events.py @@ -0,0 +1,31 @@ +from crewai.utilities.events.base_events import BaseEvent + + +class AgentReasoningStartedEvent(BaseEvent): + """Event emitted when an agent starts reasoning about a task.""" + + type: str = "agent_reasoning_started" + agent_role: str + task_id: str + attempt: int = 1 # The current reasoning/refinement attempt + + +class AgentReasoningCompletedEvent(BaseEvent): + """Event emitted when an agent finishes its reasoning process.""" + + type: str = "agent_reasoning_completed" + agent_role: str + task_id: str + plan: str + ready: bool + attempt: int = 1 + + +class AgentReasoningFailedEvent(BaseEvent): + """Event emitted when the reasoning process fails.""" + + type: str = "agent_reasoning_failed" + agent_role: str + task_id: str + error: str + attempt: int = 1 \ No newline at end of file diff --git a/src/crewai/utilities/events/utils/console_formatter.py b/src/crewai/utilities/events/utils/console_formatter.py index b9adc9fda..e3ae471d2 100644 --- a/src/crewai/utilities/events/utils/console_formatter.py +++ b/src/crewai/utilities/events/utils/console_formatter.py @@ -15,6 +15,7 @@ class ConsoleFormatter: current_method_branch: Optional[Tree] = None current_lite_agent_branch: Optional[Tree] = None tool_usage_counts: Dict[str, int] = {} + current_reasoning_branch: Optional[Tree] = None # Track reasoning status def __init__(self, verbose: bool = False): self.console = Console(width=None) @@ -399,7 +400,11 @@ class ConsoleFormatter: tree_to_use = branch_to_use or crew_tree if branch_to_use is None or tree_to_use is None: - return None + # If we don't have a valid branch, default to crew_tree if provided + if crew_tree is not None: + branch_to_use = tree_to_use = crew_tree + else: + return None # Update tool usage count self.tool_usage_counts[tool_name] = self.tool_usage_counts.get(tool_name, 0) + 1 @@ -501,7 +506,11 @@ class ConsoleFormatter: tree_to_use = branch_to_use or crew_tree if branch_to_use is None or tree_to_use is None: - return None + # If we don't have a valid branch, default to crew_tree if provided + if crew_tree is not None: + branch_to_use = tree_to_use = crew_tree + else: + return None # Only add thinking status if we don't have a current tool branch if self.current_tool_branch is None: @@ -797,7 +806,7 @@ class ConsoleFormatter: tree_to_use = branch_to_use or crew_tree if branch_to_use is None or tree_to_use is None: - # If we don't have a valid branch, use crew_tree as the branch if available + # If we don't have a valid branch, default to crew_tree if provided if crew_tree is not None: branch_to_use = tree_to_use = crew_tree else: @@ -982,3 +991,118 @@ class ConsoleFormatter: "Knowledge Search Failed", "Search Error", "red", Error=error ) self.print_panel(error_content, "Search Error", "red") + + # ----------- AGENT REASONING EVENTS ----------- + + def handle_reasoning_started( + self, + agent_branch: Optional[Tree], + attempt: int, + crew_tree: Optional[Tree], + ) -> Optional[Tree]: + """Handle agent reasoning started (or refinement) event.""" + if not self.verbose: + return None + + # Prefer LiteAgent branch if we are within a LiteAgent context + branch_to_use = self.current_lite_agent_branch or agent_branch + tree_to_use = branch_to_use or crew_tree + + if branch_to_use is None or tree_to_use is None: + # If we don't have a valid branch, default to crew_tree if provided + if crew_tree is not None: + branch_to_use = tree_to_use = crew_tree + else: + return None + + # Reuse existing reasoning branch if present + reasoning_branch = self.current_reasoning_branch + if reasoning_branch is None: + reasoning_branch = branch_to_use.add("") + self.current_reasoning_branch = reasoning_branch + + # Build label text depending on attempt + status_text = ( + f"Reasoning (Attempt {attempt})" if attempt > 1 else "Reasoning..." + ) + self.update_tree_label(reasoning_branch, "🧠", status_text, "blue") + + self.print(tree_to_use) + self.print() + + return reasoning_branch + + def handle_reasoning_completed( + self, + plan: str, + ready: bool, + crew_tree: Optional[Tree], + ) -> None: + """Handle agent reasoning completed event.""" + if not self.verbose: + return + + reasoning_branch = self.current_reasoning_branch + tree_to_use = ( + self.current_lite_agent_branch + or self.current_agent_branch + or crew_tree + ) + + style = "green" if ready else "yellow" + status_text = "Reasoning Completed" if ready else "Reasoning Completed (Not Ready)" + + if reasoning_branch is not None: + self.update_tree_label(reasoning_branch, "✅", status_text, style) + + if tree_to_use is not None: + self.print(tree_to_use) + + # Show plan in a panel (trim very long plans) + if plan: + plan_panel = Panel( + Text(plan, style="white"), + title="🧠 Reasoning Plan", + border_style=style, + padding=(1, 2), + ) + self.print(plan_panel) + + self.print() + + # Clear stored branch after completion + self.current_reasoning_branch = None + + def handle_reasoning_failed( + self, + error: str, + crew_tree: Optional[Tree], + ) -> None: + """Handle agent reasoning failure event.""" + if not self.verbose: + return + + reasoning_branch = self.current_reasoning_branch + tree_to_use = ( + self.current_lite_agent_branch + or self.current_agent_branch + or crew_tree + ) + + if reasoning_branch is not None: + self.update_tree_label(reasoning_branch, "❌", "Reasoning Failed", "red") + + if tree_to_use is not None: + self.print(tree_to_use) + + # Error panel + error_content = self.create_status_content( + "Reasoning Failed", + "Error", + "red", + Error=error, + ) + self.print_panel(error_content, "Reasoning Error", "red") + + # Clear stored branch after failure + self.current_reasoning_branch = None diff --git a/src/crewai/utilities/reasoning_handler.py b/src/crewai/utilities/reasoning_handler.py index 2fa5dba3c..cb94eb38a 100644 --- a/src/crewai/utilities/reasoning_handler.py +++ b/src/crewai/utilities/reasoning_handler.py @@ -8,6 +8,12 @@ from crewai.agent import Agent from crewai.task import Task from crewai.utilities import I18N from crewai.llm import LLM +from crewai.utilities.events.crewai_event_bus import crewai_event_bus +from crewai.utilities.events.reasoning_events import ( + AgentReasoningStartedEvent, + AgentReasoningCompletedEvent, + AgentReasoningFailedEvent, +) class ReasoningPlan(BaseModel): @@ -49,7 +55,55 @@ class AgentReasoning: Returns: AgentReasoningOutput: The output of the agent reasoning process. """ - return self.__handle_agent_reasoning() + # Emit a reasoning started event (attempt 1) + try: + crewai_event_bus.emit( + self.agent, + AgentReasoningStartedEvent( + agent_role=self.agent.role, + task_id=str(self.task.id), + attempt=1, + ), + ) + except Exception: + # Ignore event bus errors to avoid breaking execution + pass + + try: + output = self.__handle_agent_reasoning() + + # Emit reasoning completed event + try: + crewai_event_bus.emit( + self.agent, + AgentReasoningCompletedEvent( + agent_role=self.agent.role, + task_id=str(self.task.id), + plan=output.plan.plan, + ready=output.plan.ready, + attempt=1, + ), + ) + except Exception: + pass + + return output + except Exception as e: + # Emit reasoning failed event + try: + crewai_event_bus.emit( + self.agent, + AgentReasoningFailedEvent( + agent_role=self.agent.role, + task_id=str(self.task.id), + error=str(e), + attempt=1, + ), + ) + except Exception: + pass + + raise def __handle_agent_reasoning(self) -> AgentReasoningOutput: """ @@ -108,6 +162,19 @@ class AgentReasoning: max_attempts = self.agent.max_reasoning_attempts while not ready and (max_attempts is None or attempt < max_attempts): + # Emit event for each refinement attempt + try: + crewai_event_bus.emit( + self.agent, + AgentReasoningStartedEvent( + agent_role=self.agent.role, + task_id=str(self.task.id), + attempt=attempt + 1, + ), + ) + except Exception: + pass + refine_prompt = self.__create_refine_prompt(plan) if self.llm.supports_function_calling(): @@ -179,12 +246,18 @@ class AgentReasoning: backstory=self.__get_agent_backstory() ) + # Prepare a simple callable that just returns the tool arguments as JSON + def _create_reasoning_plan(plan: str, ready: bool): # noqa: N802 + """Return the reasoning plan result in JSON string form.""" + return json.dumps({"plan": plan, "ready": ready}) + response = self.llm.call( [ {"role": "system", "content": system_prompt}, {"role": "user", "content": prompt} ], - tools=[function_schema] + tools=[function_schema], + available_functions={"create_reasoning_plan": _create_reasoning_plan}, ) self.logger.debug(f"Function calling response: {response[:100]}...")