From d1f168af0ad7a0693a5cc108a69979b3d1e9298a Mon Sep 17 00:00:00 2001 From: Ritika shrestha <87307821+ritsth@users.noreply.github.com> Date: Thu, 2 Jul 2026 20:39:24 -0700 Subject: [PATCH] fix(agents): substitute prompt placeholders in a single pass AgentExecutor._format_prompt replaced {input}, {tool_names} and {tools} sequentially, so a value substituted earlier that itself contained a later placeholder token was clobbered. Task input mentioning the literal {tools} or {tool_names} therefore had those tokens overwritten with the tool list/names. Substitute all placeholders in one re.sub pass so inserted values are not re-scanned. Adds a regression test. --- .../src/crewai/experimental/agent_executor.py | 13 ++++++++--- .../tests/agents/test_agent_executor.py | 23 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/lib/crewai/src/crewai/experimental/agent_executor.py b/lib/crewai/src/crewai/experimental/agent_executor.py index 0ecd8e63a4..151dcd183e 100644 --- a/lib/crewai/src/crewai/experimental/agent_executor.py +++ b/lib/crewai/src/crewai/experimental/agent_executor.py @@ -8,6 +8,7 @@ from datetime import datetime import inspect import json +import re import threading from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast from uuid import uuid4 @@ -3149,6 +3150,10 @@ async def _ainject_files_from_inputs(self, inputs: dict[str, Any]) -> None: def _format_prompt(prompt: str, inputs: dict[str, str]) -> str: """Format prompt template with input values. + Substitutes every placeholder in a single pass so a substituted value + that itself contains a placeholder token (e.g. task input mentioning + the literal ``{tools}``) is not clobbered by a later replacement. + Args: prompt: Template string. inputs: Values to substitute. @@ -3156,9 +3161,11 @@ def _format_prompt(prompt: str, inputs: dict[str, str]) -> str: Returns: Formatted prompt. """ - prompt = prompt.replace("{input}", inputs["input"]) - prompt = prompt.replace("{tool_names}", inputs["tool_names"]) - return prompt.replace("{tools}", inputs["tools"]) + return re.sub( + r"\{(input|tool_names|tools)\}", + lambda match: inputs[match.group(1)], + prompt, + ) def _handle_human_feedback(self, formatted_answer: AgentFinish) -> AgentFinish: """Process human feedback and refine answer. diff --git a/lib/crewai/tests/agents/test_agent_executor.py b/lib/crewai/tests/agents/test_agent_executor.py index e4de4a484b..3af214ea06 100644 --- a/lib/crewai/tests/agents/test_agent_executor.py +++ b/lib/crewai/tests/agents/test_agent_executor.py @@ -475,6 +475,29 @@ def test_format_prompt(self, mock_dependencies): assert "tool1, tool2" in result assert "desc" in result + def test_format_prompt_preserves_placeholder_tokens_in_values( + self, mock_dependencies + ): + """A value containing a placeholder token must not be re-substituted. + + Regression: sequential ``str.replace`` substituted ``{input}`` first, so + task input that legitimately mentioned ``{tools}`` or ``{tool_names}`` + had those tokens clobbered by the later replacements. + """ + executor = _build_executor(**mock_dependencies) + inputs = { + "input": "explain the {tools} and {tool_names} placeholders", + "tool_names": "search", + "tools": "search: web search", + } + + result = executor._format_prompt("Task: {input}\nTools: {tools}", inputs) + + assert result == ( + "Task: explain the {tools} and {tool_names} placeholders\n" + "Tools: search: web search" + ) + def test_is_training_mode_false(self, mock_dependencies): """Test training mode detection when not in training.""" executor = _build_executor(**mock_dependencies)