[BUG] Custom prompt templates always inject the ReAct `task` slice — wrong instructions for no-tool and native-tool agents
Description
Prompts._build_prompt has two code paths: the default one joins the i18n slices selected in task_execution (role_playing, plus tools/no_tools, plus exactly one task slice: task, native_task, or task_no_tools). The custom-template path (taken when both system_template and prompt_template are set) ignores which task slice was selected:
{{ .System }}is filled with every component except the literal"task", sonative_taskandtask_no_toolsare not excluded — the selected task slice leaks into the system section.{{ .Prompt }}is always filled with the hardcoded"task"slice regardless of the selection.
The "task" slice contains ReAct-only instructions: "use the tools available and give your best Final Answer, your job depends on it!" and a Thought: seed. The result:
- An agent with no tools (
task_no_toolsselected) is told to "use the tools available" and gets aThought:ReAct scaffold — the exact thought-leakage problem theuse_system_promptpath was fixed for. - An agent using native function calling (
native_taskselected) gets text ReAct instructions that conflict with the native calling flow. - In both cases
Current Task: {input}is emitted twice — once via the unfiltered task slice in{{ .System }}and again via the hardcodedtaskslice in{{ .Prompt }}.
Only the tool-enabled ReAct configuration (task selected) produces correct output today.
Steps to Reproduce
from unittest.mock import MagicMock
from crewai.utilities.prompts import Prompts
agent = MagicMock()
agent.role, agent.goal, agent.backstory = "Writer", "Write", "Pro"
# No tools + custom templates
prompt = Prompts(
has_tools=False, use_native_tool_calling=False, use_system_prompt=False,
agent=agent,
system_template="system:\n{{ .System }}",
prompt_template="user:\n{{ .Prompt }}",
).task_execution()["prompt"]
print(prompt)
# Actual (buggy): system section contains "Current Task: {input}\nProvide your
# complete response:" AND the user section contains the "task" slice:
# "use the tools available and give your best Final Answer ... Thought:"
# Expected: user section contains "task_no_tools" ("Provide your complete
# response:"), no tool instructions, "Current Task:" appears exactly once.
# Same failure with native tool calling:
prompt = Prompts(
has_tools=True, use_native_tool_calling=True, use_system_prompt=False,
agent=agent,
system_template="system:\n{{ .System }}",
prompt_template="user:\n{{ .Prompt }}",
).task_execution()["prompt"]
# Actual (buggy): user section contains the ReAct "task" slice
# ("Thought:" seed, "use the tools available"), and "Current Task: {input}"
# appears twice.The regression tests in lib/crewai/tests/utilities/test_prompts_no_thought_leakage.py (TestCustomTemplatesPromptGeneration) reproduce both cases — they fail on the unpatched code and pass with the fix; a third test pins the unchanged ReAct-with-tools behavior.
Expected behavior
With custom system_template / prompt_template, {{ .Prompt }} is filled with the task slice that task_execution selected (task_no_tools for an agent without tools, native_task for native tool calling, task otherwise), and that slice does not also appear in {{ .System }}.
Screenshots/Code snippets
lib/crewai/src/crewai/utilities/prompts.py, Prompts._build_prompt, custom-template branch:
- Line 239:
if component != "task"— only excludes thetaskslice, notnative_task/task_no_tools. - Line 245:
"{{ .Prompt }}", "".join(I18N_DEFAULT.slice("task"))— hardcodes thetaskslice.
components is built in task_execution (lines 99–118) which appends one of task / native_task / task_no_tools last based on has_tools and use_native_tool_calling. Reached from Agent._build_execution_prompt (lib/crewai/src/crewai/agent/core.py:1133) whenever an agent sets system_template + prompt_template.
Operating System
Ubuntu 22.04
Python Version
3.12
crewAI Version
1.15.22 (main @ 5c33fe4)
crewAI Tools Version
1.15.22
Virtual Environment
Venv
Evidence
Any agent configured with custom system_template/prompt_template that either has no tools or uses native tool calling receives a prompt that (a) instructs it to use tools it does not have / should not call textually, (b) seeds a Thought: prefix that leaks into the final output, and (c) duplicates the Current Task: block. This silently degrades output quality on a documented configuration path (Agent(system_template=..., prompt_template=...)).
Possible Solution
In the custom-template branch, treat all three task-family slices (task, native_task, task_no_tools) as the task component: exclude all of them from the {{ .System }} parts, and fill {{ .Prompt }} with whichever one is present in components (falling back to "task" when none is, preserving prior behavior):
task_components = {"task", "native_task", "task_no_tools"}
template_parts = [
I18N_DEFAULT.slice(c) for c in components if c not in task_components
]
system = system_template.replace("{{ .System }}", "".join(template_parts))
task_component = next((c for c in components if c in task_components), "task")
prompt = prompt_template.replace("{{ .Prompt }}", I18N_DEFAULT.slice(task_component))Additional context
Found and written with an AI coding agent (Devin, reviewed by Claude Code); please apply the llm-generated label. A fix with regression tests is ready and will be linked here.
Source: crewAIInc/crewAI