diff --git a/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/openai_agent_tool_adapter.py b/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/openai_agent_tool_adapter.py index 7543305f0..7e0979ba5 100644 --- a/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/openai_agent_tool_adapter.py +++ b/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/openai_agent_tool_adapter.py @@ -99,12 +99,10 @@ class OpenAIAgentToolAdapter(BaseToolAdapter): Returns: Tool execution result. """ - # Get the parameter name from the schema param_name: str = next( iter(tool.args_schema.model_json_schema()["properties"].keys()) ) - # Handle different argument types args_dict: dict[str, Any] if isinstance(arguments, dict): args_dict = arguments @@ -116,16 +114,13 @@ class OpenAIAgentToolAdapter(BaseToolAdapter): else: args_dict = {param_name: str(arguments)} - # Run the tool with the processed arguments output: Any | Awaitable[Any] = tool._run(**args_dict) - # Await if the tool returned a coroutine if inspect.isawaitable(output): result: Any = await output else: result = output - # Ensure the result is JSON serializable if isinstance(result, (dict, list, str, int, float, bool, type(None))): return result return str(result) diff --git a/lib/crewai/src/crewai/agents/agent_builder/base_agent.py b/lib/crewai/src/crewai/agents/agent_builder/base_agent.py index 9d37373e8..a00f9b49f 100644 --- a/lib/crewai/src/crewai/agents/agent_builder/base_agent.py +++ b/lib/crewai/src/crewai/agents/agent_builder/base_agent.py @@ -383,7 +383,6 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta): if isinstance(tool, BaseTool): processed_tools.append(tool) elif all(hasattr(tool, attr) for attr in required_attrs): - # Tool has the required attributes, create a Tool instance processed_tools.append(Tool.from_langchain(tool)) else: raise ValueError( @@ -448,14 +447,12 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta): @model_validator(mode="after") def validate_and_set_attributes(self) -> Self: - # Validate required fields for field in ["role", "goal", "backstory"]: if getattr(self, field) is None: raise ValueError( f"{field} must be provided either directly or through config" ) - # Set private attributes self._logger = Logger(verbose=self.verbose) if self.max_rpm and not self._rpm_controller: self._rpm_controller = RPMController( @@ -464,7 +461,6 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta): if not self._token_process: self._token_process = TokenProcess() - # Initialize security_config if not provided if self.security_config is None: self.security_config = SecurityConfig() @@ -566,14 +562,11 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta): "actions", } - # Copy llm existing_llm = shallow_copy(self.llm) copied_knowledge = shallow_copy(self.knowledge) copied_knowledge_storage = shallow_copy(self.knowledge_storage) - # Properly copy knowledge sources if they exist existing_knowledge_sources = None if self.knowledge_sources: - # Create a shared storage instance for all knowledge sources shared_storage = ( self.knowledge_sources[0].storage if self.knowledge_sources else None ) @@ -585,7 +578,6 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta): if hasattr(source, "model_copy") else shallow_copy(source) ) - # Ensure all copied sources use the same storage instance copied_source.storage = shared_storage existing_knowledge_sources.append(copied_source) diff --git a/lib/crewai/src/crewai/agents/constants.py b/lib/crewai/src/crewai/agents/constants.py index 326d53d02..7a180f947 100644 --- a/lib/crewai/src/crewai/agents/constants.py +++ b/lib/crewai/src/crewai/agents/constants.py @@ -4,8 +4,6 @@ import re from typing import Final -# crewai.agents.parser constants - FINAL_ANSWER_ACTION: Final[str] = "Final Answer:" MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE: Final[str] = ( "I did it wrong. Invalid Format: I missed the 'Action:' after 'Thought:'. I will do right next, and don't use a tool I have already used.\n" diff --git a/lib/crewai/src/crewai/agents/crew_agent_executor.py b/lib/crewai/src/crewai/agents/crew_agent_executor.py index b98840cae..62369bfb9 100644 --- a/lib/crewai/src/crewai/agents/crew_agent_executor.py +++ b/lib/crewai/src/crewai/agents/crew_agent_executor.py @@ -296,7 +296,6 @@ class CrewAgentExecutor(BaseAgentExecutor): Returns: Final answer from the agent. """ - # Check if model supports native function calling use_native_tools = ( hasattr(self.llm, "supports_function_calling") and callable(getattr(self.llm, "supports_function_calling", None)) @@ -307,7 +306,6 @@ class CrewAgentExecutor(BaseAgentExecutor): if use_native_tools: return self._invoke_loop_native_tools() - # Fall back to ReAct text-based pattern return self._invoke_loop_react() def _invoke_loop_react(self) -> AgentFinish: @@ -347,7 +345,6 @@ class CrewAgentExecutor(BaseAgentExecutor): executor_context=self, verbose=self.agent.verbose, ) - # breakpoint() if self.response_model is not None: try: if isinstance(answer, BaseModel): @@ -365,7 +362,6 @@ class CrewAgentExecutor(BaseAgentExecutor): text=answer, ) except ValidationError: - # If validation fails, convert BaseModel to JSON string for parsing answer_str = ( answer.model_dump_json() if isinstance(answer, BaseModel) @@ -375,14 +371,12 @@ class CrewAgentExecutor(BaseAgentExecutor): answer_str, self.use_stop_words ) # type: ignore[assignment] else: - # When no response_model, answer should be a string answer_str = str(answer) if not isinstance(answer, str) else answer formatted_answer = process_llm_response( answer_str, self.use_stop_words ) # type: ignore[assignment] if isinstance(formatted_answer, AgentAction): - # Extract agent fingerprint if available fingerprint_context = {} if ( self.agent @@ -426,7 +420,6 @@ class CrewAgentExecutor(BaseAgentExecutor): except Exception as e: if e.__class__.__module__.startswith("litellm"): - # Do not retry on litellm errors raise e if is_context_length_exceeded(e): handle_context_length( @@ -443,10 +436,6 @@ class CrewAgentExecutor(BaseAgentExecutor): finally: self.iterations += 1 - # During the invoke loop, formatted_answer alternates between AgentAction - # (when the agent is using tools) and eventually becomes AgentFinish - # (when the agent reaches a final answer). This check confirms we've - # reached a final answer and helps type checking understand this transition. if not isinstance(formatted_answer, AgentFinish): raise RuntimeError( "Agent execution ended without reaching a final answer. " @@ -465,9 +454,7 @@ class CrewAgentExecutor(BaseAgentExecutor): Returns: Final answer from the agent. """ - # Convert tools to OpenAI schema format if not self.original_tools: - # No tools available, fall back to simple LLM call return self._invoke_loop_native_no_tools() openai_tools, available_functions, self._tool_name_mapping = ( @@ -490,10 +477,6 @@ class CrewAgentExecutor(BaseAgentExecutor): enforce_rpm_limit(self.request_within_rpm_limit) - # Call LLM with native tools - # Pass available_functions=None so the LLM returns tool_calls - # without executing them. The executor handles tool execution - # via _handle_native_tool_calls to properly manage message history. answer = get_llm_response( llm=cast("BaseLLM", self.llm), messages=self.messages, @@ -508,32 +491,26 @@ class CrewAgentExecutor(BaseAgentExecutor): verbose=self.agent.verbose, ) - # Check if the response is a list of tool calls if ( isinstance(answer, list) and answer and self._is_tool_call_list(answer) ): - # Handle tool calls - execute tools and add results to messages tool_finish = self._handle_native_tool_calls( answer, available_functions ) - # If tool has result_as_answer=True, return immediately if tool_finish is not None: return tool_finish - # Continue loop to let LLM analyze results and decide next steps continue - # Text or other response - handle as potential final answer if isinstance(answer, str): - # Text response - this is the final answer formatted_answer = AgentFinish( thought="", output=answer, text=answer, ) self._invoke_step_callback(formatted_answer) - self._append_message(answer) # Save final answer to messages + self._append_message(answer) self._show_logs(formatted_answer) return formatted_answer @@ -549,14 +526,13 @@ class CrewAgentExecutor(BaseAgentExecutor): self._show_logs(formatted_answer) return formatted_answer - # Unexpected response type, treat as final answer formatted_answer = AgentFinish( thought="", output=str(answer), text=str(answer), ) self._invoke_step_callback(formatted_answer) - self._append_message(str(answer)) # Save final answer to messages + self._append_message(str(answer)) self._show_logs(formatted_answer) return formatted_answer @@ -627,12 +603,10 @@ class CrewAgentExecutor(BaseAgentExecutor): if not response: return False first_item = response[0] - # OpenAI-style if hasattr(first_item, "function") or ( isinstance(first_item, dict) and "function" in first_item ): return True - # Anthropic-style (object with attributes) if ( hasattr(first_item, "type") and getattr(first_item, "type", None) == "tool_use" @@ -640,14 +614,12 @@ class CrewAgentExecutor(BaseAgentExecutor): return True if hasattr(first_item, "name") and hasattr(first_item, "input"): return True - # Bedrock-style (dict with name and input keys) if ( isinstance(first_item, dict) and "name" in first_item and "input" in first_item ): return True - # Gemini-style if hasattr(first_item, "function_call") and first_item.function_call: return True return False @@ -706,8 +678,6 @@ class CrewAgentExecutor(BaseAgentExecutor): for _, func_name, _ in parsed_calls ) - # Preserve historical sequential behavior for result_as_answer batches. - # Also avoid threading around usage counters for max_usage_count tools. if has_result_as_answer_in_batch or has_max_usage_count_in_batch: logger.debug( "Skipping parallel native execution because batch includes result_as_answer or max_usage_count tool" @@ -773,7 +743,6 @@ class CrewAgentExecutor(BaseAgentExecutor): self.messages.append(reasoning_message) return None - # Sequential behavior: process only first tool call, then force reflection. call_id, func_name, func_args = parsed_calls[0] self._append_assistant_tool_calls_message([(call_id, func_name, func_args)]) @@ -1202,7 +1171,6 @@ class CrewAgentExecutor(BaseAgentExecutor): text=answer, ) except ValidationError: - # If validation fails, convert BaseModel to JSON string for parsing answer_str = ( answer.model_dump_json() if isinstance(answer, BaseModel) @@ -1212,7 +1180,6 @@ class CrewAgentExecutor(BaseAgentExecutor): answer_str, self.use_stop_words ) # type: ignore[assignment] else: - # When no response_model, answer should be a string answer_str = str(answer) if not isinstance(answer, str) else answer formatted_answer = process_llm_response( answer_str, self.use_stop_words @@ -1319,10 +1286,6 @@ class CrewAgentExecutor(BaseAgentExecutor): enforce_rpm_limit(self.request_within_rpm_limit) - # Call LLM with native tools - # Pass available_functions=None so the LLM returns tool_calls - # without executing them. The executor handles tool execution - # via _handle_native_tool_calls to properly manage message history. answer = await aget_llm_response( llm=cast("BaseLLM", self.llm), messages=self.messages, @@ -1336,32 +1299,26 @@ class CrewAgentExecutor(BaseAgentExecutor): executor_context=self, verbose=self.agent.verbose, ) - # Check if the response is a list of tool calls if ( isinstance(answer, list) and answer and self._is_tool_call_list(answer) ): - # Handle tool calls - execute tools and add results to messages tool_finish = self._handle_native_tool_calls( answer, available_functions ) - # If tool has result_as_answer=True, return immediately if tool_finish is not None: return tool_finish - # Continue loop to let LLM analyze results and decide next steps continue - # Text or other response - handle as potential final answer if isinstance(answer, str): - # Text response - this is the final answer formatted_answer = AgentFinish( thought="", output=answer, text=answer, ) await self._ainvoke_step_callback(formatted_answer) - self._append_message(answer) # Save final answer to messages + self._append_message(answer) self._show_logs(formatted_answer) return formatted_answer @@ -1377,14 +1334,13 @@ class CrewAgentExecutor(BaseAgentExecutor): self._show_logs(formatted_answer) return formatted_answer - # Unexpected response type, treat as final answer formatted_answer = AgentFinish( thought="", output=str(answer), text=str(answer), ) await self._ainvoke_step_callback(formatted_answer) - self._append_message(str(answer)) # Save final answer to messages + self._append_message(str(answer)) self._show_logs(formatted_answer) return formatted_answer @@ -1455,7 +1411,6 @@ class CrewAgentExecutor(BaseAgentExecutor): Returns: Updated action or final answer. """ - # Special case for add_image_tool add_image_tool = I18N_DEFAULT.tools("add_image") if ( isinstance(add_image_tool, dict) @@ -1575,17 +1530,14 @@ class CrewAgentExecutor(BaseAgentExecutor): training_handler = CrewTrainingHandler(TRAINING_DATA_FILE) training_data = training_handler.load() or {} - # Initialize or retrieve agent's training data agent_training_data = training_data.get(agent_id, {}) if human_feedback is not None: - # Save initial output and human feedback agent_training_data[train_iteration] = { "initial_output": result.output, "human_feedback": human_feedback, } else: - # Save improved output if train_iteration in agent_training_data: agent_training_data[train_iteration]["improved_output"] = result.output else: @@ -1599,7 +1551,6 @@ class CrewAgentExecutor(BaseAgentExecutor): ) return - # Update the training data and save training_data[agent_id] = agent_training_data training_handler.save(training_data) diff --git a/lib/crewai/src/crewai/agents/parser.py b/lib/crewai/src/crewai/agents/parser.py index bfe3a28b5..c59719226 100644 --- a/lib/crewai/src/crewai/agents/parser.py +++ b/lib/crewai/src/crewai/agents/parser.py @@ -94,11 +94,8 @@ def parse(text: str) -> AgentAction | AgentFinish: if includes_answer: final_answer = text.split(FINAL_ANSWER_ACTION)[-1].strip() - # Check whether the final answer ends with triple backticks. if final_answer.endswith("```"): - # Count occurrences of triple backticks in the final answer. count = final_answer.count("```") - # If count is odd then it's an unmatched trailing set; remove it. if count % 2 != 0: final_answer = final_answer[:-3].rstrip() return AgentFinish(thought=thought, output=final_answer, text=text) @@ -146,7 +143,6 @@ def _extract_thought(text: str) -> str: if thought_index == -1: return "" thought = text[:thought_index].strip() - # Remove any triple backticks from the thought string return thought.replace("```", "").strip() @@ -171,18 +167,9 @@ def _safe_repair_json(tool_input: str) -> str: Returns: The repaired JSON string or original if repair fails. """ - # Skip repair if the input starts and ends with square brackets - # Explanation: The JSON parser has issues handling inputs that are enclosed in square brackets ('[]'). - # These are typically valid JSON arrays or strings that do not require repair. Attempting to repair such inputs - # might lead to unintended alterations, such as wrapping the entire input in additional layers or modifying - # the structure in a way that changes its meaning. By skipping the repair for inputs that start and end with - # square brackets, we preserve the integrity of these valid JSON structures and avoid unnecessary modifications. if tool_input.startswith("[") and tool_input.endswith("]"): return tool_input - # Before repair, handle common LLM issues: - # 1. Replace """ with " to avoid JSON parser errors - tool_input = tool_input.replace('"""', '"') result = repair_json(tool_input) diff --git a/lib/crewai/src/crewai/agents/planner_observer.py b/lib/crewai/src/crewai/agents/planner_observer.py index 0705d3013..29d586663 100644 --- a/lib/crewai/src/crewai/agents/planner_observer.py +++ b/lib/crewai/src/crewai/agents/planner_observer.py @@ -83,10 +83,6 @@ class PlannerObserver: return create_llm(config.llm) return self.agent.llm - # ------------------------------------------------------------------ - # Public API - # ------------------------------------------------------------------ - def observe( self, completed_step: TodoItem, @@ -182,9 +178,6 @@ class PlannerObserver: ), ) - # Don't force a full replan — the step may have succeeded even if the - # observer LLM failed to parse the result. Defaulting to "continue" is - # far less disruptive than wiping the entire plan on every observer error. return StepObservation( step_completed_successfully=True, key_information_learned="", @@ -221,10 +214,6 @@ class PlannerObserver: return remaining_todos - # ------------------------------------------------------------------ - # Internal: Message building - # ------------------------------------------------------------------ - def _build_observation_messages( self, completed_step: TodoItem, @@ -239,15 +228,11 @@ class PlannerObserver: task_desc = self.task.description or "" task_goal = self.task.expected_output or "" elif self.kickoff_input: - # Standalone kickoff path — no Task object, but we have the raw input. - # Extract just the ## Task section so the observer sees the actual goal, - # not the full enriched instruction with env/tools/verification noise. task_desc = extract_task_section(self.kickoff_input) task_goal = "Complete the task successfully" system_prompt = I18N_DEFAULT.retrieve("planning", "observation_system_prompt") - # Build context of what's been done completed_summary = "" if all_completed: completed_lines = [] @@ -261,7 +246,6 @@ class PlannerObserver: completed_lines ) - # Build remaining plan remaining_summary = "" if remaining_todos: remaining_lines = [ @@ -306,17 +290,14 @@ class PlannerObserver: if isinstance(response, StepObservation): return response - # JSON string path — most common miss before this fix if isinstance(response, str): text = response.strip() try: return StepObservation.model_validate_json(text) except Exception: # noqa: S110 pass - # Some LLMs wrap the JSON in markdown fences if text.startswith("```"): lines = text.split("\n") - # Strip first and last lines (``` markers) inner = "\n".join( lines[1:-1] if lines[-1].strip() == "```" else lines[1:] ) @@ -325,14 +306,12 @@ class PlannerObserver: except Exception: # noqa: S110 pass - # Dict path if isinstance(response, dict): try: return StepObservation.model_validate(response) except Exception: # noqa: S110 pass - # Last resort — log what we got so it's diagnosable logger.warning( "Could not parse observation response (type=%s). " "Falling back to default failure observation. Preview: %.200s", diff --git a/lib/crewai/src/crewai/agents/step_executor.py b/lib/crewai/src/crewai/agents/step_executor.py index 3348692b6..df834e3e4 100644 --- a/lib/crewai/src/crewai/agents/step_executor.py +++ b/lib/crewai/src/crewai/agents/step_executor.py @@ -108,7 +108,6 @@ class StepExecutor: self.request_within_rpm_limit = request_within_rpm_limit self.callbacks = callbacks or [] - # Native tool support — set up once self._use_native_tools = check_native_tool_support( self.llm, self.original_tools ) @@ -121,10 +120,6 @@ class StepExecutor: _, ) = setup_native_tools(self.original_tools) - # ------------------------------------------------------------------ - # Public API - # ------------------------------------------------------------------ - def execute( self, todo: TodoItem, @@ -190,10 +185,6 @@ class StepExecutor: execution_time=elapsed, ) - # ------------------------------------------------------------------ - # Internal: Message building - # ------------------------------------------------------------------ - def _build_isolated_messages( self, todo: TodoItem, context: StepExecutionContext ) -> list[LLMMessage]: @@ -237,10 +228,6 @@ class StepExecutor: """Build the user prompt for this specific step.""" parts: list[str] = [] - # Include overall task context so the executor knows the full goal and - # required output format/location — critical for knowing WHAT to produce. - # We extract only the task body (not tool instructions or verification - # sections) to avoid duplicating directives already in the system prompt. if context.task_description: task_section = extract_task_section(context.task_description) if task_section: @@ -267,7 +254,6 @@ class StepExecutor: ) ) - # Include dependency results (final results only, no traces) if context.dependency_results: parts.append( I18N_DEFAULT.retrieve("planning", "step_executor_context_header") @@ -283,10 +269,6 @@ class StepExecutor: return "\n".join(parts) - # ------------------------------------------------------------------ - # Internal: Multi-turn execution loop - # ------------------------------------------------------------------ - def _execute_text_parsed( self, messages: list[LLMMessage], @@ -306,7 +288,6 @@ class StepExecutor: last_tool_result = "" for _ in range(max_step_iterations): - # Check step timeout if step_timeout and start_time: elapsed = time.monotonic() - start_time if elapsed >= step_timeout: @@ -331,17 +312,12 @@ class StepExecutor: tool_calls_made.append(formatted.tool) tool_result = self._execute_text_tool_with_events(formatted) last_tool_result = tool_result - # Append the assistant's reasoning + action, then the observation. - # _build_observation_message handles vision sentinels so the LLM - # receives an image content block instead of raw base64 text. messages.append({"role": "assistant", "content": answer_str}) messages.append(self._build_observation_message(tool_result)) continue - # Raw text response with no Final Answer marker — treat as done return answer_str - # Max iterations reached — return the last tool result we accumulated return last_tool_result def _execute_text_tool_with_events(self, formatted: AgentAction) -> str: @@ -429,10 +405,6 @@ class StepExecutor: return {"input": stripped_input} return {"input": str(tool_input)} - # ------------------------------------------------------------------ - # Internal: Vision support - # ------------------------------------------------------------------ - @staticmethod def _parse_vision_sentinel(raw: str) -> tuple[str, str] | None: """Parse a VISION_IMAGE sentinel into (media_type, base64_data), or None.""" @@ -517,7 +489,6 @@ class StepExecutor: accumulated_results: list[str] = [] for _ in range(max_step_iterations): - # Check step timeout if step_timeout and start_time: elapsed = time.monotonic() - start_time if elapsed >= step_timeout: @@ -541,19 +512,14 @@ class StepExecutor: return answer.model_dump_json() if isinstance(answer, list) and answer and is_tool_call_list(answer): - # _execute_native_tool_calls appends assistant + tool messages - # to `messages` as a side-effect, so the next LLM call will - # see the full conversation history including tool outputs. result = self._execute_native_tool_calls( answer, messages, tool_calls_made ) accumulated_results.append(result) continue - # Text answer → LLM decided the step is done return str(answer) - # Max iterations reached — return everything we accumulated return "\n".join(filter(None, accumulated_results)) def _execute_native_tool_calls( @@ -599,9 +565,6 @@ class StepExecutor: parsed = self._parse_vision_sentinel(raw_content) if parsed: media_type, b64_data = parsed - # Replace the sentinel with a standard image_url content block. - # Each provider's _format_messages handles conversion to - # its native format (e.g. Anthropic image blocks). modified: LLMMessage = cast( LLMMessage, dict(call_result.tool_message) )