diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index 6a6354419b..07bfcb8bf7 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -655,8 +655,14 @@ async def invoke( if isinstance(arguments, Mapping): parsed_arguments = dict(arguments) if self.input_model is not None and not self._schema_supplied: + # exclude_unset (not exclude_none): keep arguments the model + # explicitly provided even when their value is null, and drop + # only the ones it left out, so the function's own defaults + # apply. Excluding null instead would strip a required nullable + # parameter the model deliberately set to null, failing the + # invocation on the missing argument (#5934). parsed_arguments = self.input_model.model_validate(parsed_arguments).model_dump( - exclude_none=True + exclude_unset=True ) elif isinstance(arguments, BaseModel): if ( @@ -665,7 +671,7 @@ async def invoke( and not isinstance(arguments, self.input_model) ): raise TypeError(f"Expected {self.input_model.__name__}, got {type(arguments).__name__}") - parsed_arguments = arguments.model_dump(exclude_none=True) + parsed_arguments = arguments.model_dump(exclude_unset=True) else: raise TypeError( f"Expected mapping-like arguments for tool '{self.name}', got {type(arguments).__name__}" diff --git a/python/packages/core/tests/core/test_tools.py b/python/packages/core/tests/core/test_tools.py index da632b6074..6fae8e2408 100644 --- a/python/packages/core/tests/core/test_tools.py +++ b/python/packages/core/tests/core/test_tools.py @@ -151,6 +151,33 @@ def search(query: str) -> str: await search.invoke(arguments={}) +async def test_invoke_preserves_explicit_null_argument(): + """A required nullable argument the model sets to null must reach the function. + + Regression for #5934: exclude_none dropped the explicit null, so the required + ``unit`` went missing and the invocation failed. + """ + + @tool + def get_weather(location: str, unit: Literal["C", "F"] | None) -> str: + return f"{location}:{unit}" + + result = await get_weather.invoke(arguments={"location": "Seattle", "unit": None}) + assert isinstance(result, list) + assert result[0].text == "Seattle:None" + + +async def test_invoke_omitted_optional_uses_function_default(): + """An omitted optional argument still falls back to the function's own default.""" + + @tool + def get_weather(location: str, unit: str = "C") -> str: + return f"{location}:{unit}" + + result = await get_weather.invoke(arguments={"location": "Seattle"}) + assert result[0].text == "Seattle:C" + + async def test_tool_decorator_with_json_schema_invoke_invalid_type(): """Test schema type checks run for mapping arguments."""