From 5704ea08ebc4a724b7c2b3af74efe94c1f06cef4 Mon Sep 17 00:00:00 2001 From: monkscode <87177225+monkscode@users.noreply.github.com> Date: Thu, 10 Sep 2026 13:46:16 +0530 Subject: [PATCH] fix(agents): keep null in the task output schema embedded in the prompt (#6775) * fix(agents): keep null in the task output schema embedded in the prompt build_task_prompt_with_schema embeds the task output schema into the prompt via generate_model_description, whose strip_null_types defaults to True. Combined with ensure_all_properties_required, an Optional[str] = None field reaches the model as a required, non-nullable string, contradicting the provider-side response schema generated from the same model. That sanitizer targets OpenAI strict function-calling schemas. This call site produces prompt prose, where those constraints do not apply. Pass strip_null_types=False, matching the existing call for tool schemas in utilities/agent_utils.py. Fixes #6774 * test(agents): cover the output_json branch of the prompt schema build_task_prompt_with_schema embeds a schema on both the output_json and the output_pydantic branch, and this PR changes both. The regression test only built a Task with output_pydantic, so the output_json branch shipped unpinned. Parameterize over both output attributes. Checked on this branch with the fix reverted: both cases fail on the missing anyOf, and both pass with it. Also compare the anyOf members as a set, so member order is not part of the test contract. --------- Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> --- lib/crewai/src/crewai/agent/utils.py | 8 +++- lib/crewai/tests/agents/test_agent_utils.py | 41 +++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 lib/crewai/tests/agents/test_agent_utils.py diff --git a/lib/crewai/src/crewai/agent/utils.py b/lib/crewai/src/crewai/agent/utils.py index 20eb06b47..297322a0c 100644 --- a/lib/crewai/src/crewai/agent/utils.py +++ b/lib/crewai/src/crewai/agent/utils.py @@ -74,13 +74,17 @@ def build_task_prompt_with_schema(task: Task, task_prompt: str) -> str: if (task.output_json or task.output_pydantic) and not task.response_model: if task.output_json: - schema_dict = generate_model_description(task.output_json) + schema_dict = generate_model_description( + task.output_json, strip_null_types=False + ) schema = json.dumps(schema_dict["json_schema"]["schema"], indent=2) task_prompt += "\n" + I18N_DEFAULT.slice( "formatted_task_instructions" ).format(output_format=schema) elif task.output_pydantic: - schema_dict = generate_model_description(task.output_pydantic) + schema_dict = generate_model_description( + task.output_pydantic, strip_null_types=False + ) schema = json.dumps(schema_dict["json_schema"]["schema"], indent=2) task_prompt += "\n" + I18N_DEFAULT.slice( "formatted_task_instructions" diff --git a/lib/crewai/tests/agents/test_agent_utils.py b/lib/crewai/tests/agents/test_agent_utils.py new file mode 100644 index 000000000..3d2a2152e --- /dev/null +++ b/lib/crewai/tests/agents/test_agent_utils.py @@ -0,0 +1,41 @@ +"""Tests for crewai.agent.utils prompt-building helpers.""" + +from __future__ import annotations + +import json + +import pytest +from pydantic import BaseModel, Field + +from crewai import Task +from crewai.agent.utils import build_task_prompt_with_schema + + +class _Output(BaseModel): + name: str + note: str | None = Field(default=None) + + +@pytest.mark.parametrize("output_attribute", ["output_pydantic", "output_json"]) +def test_optional_fields_stay_nullable_in_the_prompt_schema( + output_attribute: str, +) -> None: + """An Optional field must still be expressible as null in the prompt schema. + + The provider-side response schema generated from the same model allows null, + so stripping it here hands the model two contradictory contracts and leaves + it no way to say "not applicable". Both output attributes embed a schema in + the prompt, so both are pinned. + """ + task = Task(description="d", expected_output="e", **{output_attribute: _Output}) + + prompt = build_task_prompt_with_schema(task, "") + + start = prompt.index("{") + end = prompt.rindex("}", start) + 1 + schema = json.loads(prompt[start:end]) + + assert {entry["type"] for entry in schema["properties"]["note"]["anyOf"]} == { + "string", + "null", + }