From 68539d4bcea1da753169762369a81cdaf7cae959 Mon Sep 17 00:00:00 2001 From: HughhhhCoder Date: Mon, 31 Aug 2026 22:05:55 +0800 Subject: [PATCH] fix(core): preserve variadic Annotated constraints --- src/agents/function_schema.py | 11 ++++++++--- tests/test_function_schema.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/agents/function_schema.py b/src/agents/function_schema.py index 8860c15180..2f21ae23cd 100644 --- a/src/agents/function_schema.py +++ b/src/agents/function_schema.py @@ -436,11 +436,15 @@ def function_schema( # Handle different parameter kinds if param.kind == param.VAR_POSITIONAL: # e.g. *args: extend positional args + # Keep Annotated metadata on each collected value. The stripped annotation above is + # still the source of truth for tuple-shape classification, while Pydantic applies + # Field constraints from the evaluated annotation to every positional argument. + variadic_value_ann = type_hints_with_extras.get(name, ann) if get_origin(ann) is tuple: # Preserve a homogeneous tuple as the type of each positional argument. args_of_tuple = get_args(ann) if len(args_of_tuple) == 2 and args_of_tuple[1] is Ellipsis: - ann = list[ann] # type: ignore + ann = list[variadic_value_ann] # type: ignore # tuple[()] parameterizes an empty tuple and reports no args, while a bare # typing.Tuple is unparameterized and carries no element type to reject. elif hasattr(ann, "__args__"): @@ -453,7 +457,7 @@ def function_schema( ann = list[Any] else: # If user wrote *args: int, treat as List[int] - ann = list[ann] # type: ignore + ann = list[variadic_value_ann] # type: ignore # Default factory to empty list fields[name] = ( @@ -467,7 +471,8 @@ def function_schema( # annotation as the value type -- mirroring the variadic-positional handling above, # where ``*args: X`` becomes ``list[X]`` (see #4655). A bare ``**kwargs`` has ``ann`` # set to ``Any`` above, yielding ``dict[str, Any]``. - ann = dict[str, ann] # type: ignore + variadic_value_ann = type_hints_with_extras.get(name, ann) + ann = dict[str, variadic_value_ann] # type: ignore fields[name] = ( ann, diff --git a/tests/test_function_schema.py b/tests/test_function_schema.py index 1d8325d1a0..30f15fdb89 100644 --- a/tests/test_function_schema.py +++ b/tests/test_function_schema.py @@ -509,6 +509,40 @@ def test_var_positional_supported_annotations_still_build(func: Any): assert fs.params_json_schema["properties"]["args"]["type"] == "array" +def test_var_positional_preserves_annotated_field_constraints(): + def func(*args: Annotated[int, Field(gt=0)]) -> tuple[int, ...]: + return args + + fs = function_schema(func, use_docstring_info=False) + + args_schema = fs.params_json_schema["properties"]["args"] + assert args_schema["items"]["exclusiveMinimum"] == 0 + + parsed = fs.params_pydantic_model.model_validate({"args": [1, 2]}) + args, kwargs = fs.to_call_args(parsed) + assert func(*args, **kwargs) == (1, 2) + + with pytest.raises(ValidationError): + fs.params_pydantic_model.model_validate({"args": [0]}) + + +def test_var_keyword_preserves_annotated_field_constraints(): + def func(**kwargs: Annotated[str, Field(min_length=2)]) -> dict[str, str]: + return kwargs + + fs = function_schema(func, use_docstring_info=False, strict_json_schema=False) + + kwargs_schema = fs.params_json_schema["properties"]["kwargs"] + assert kwargs_schema["additionalProperties"]["minLength"] == 2 + + parsed = fs.params_pydantic_model.model_validate({"kwargs": {"first": "ok"}}) + args, kwargs = fs.to_call_args(parsed) + assert func(*args, **kwargs) == {"first": "ok"} + + with pytest.raises(ValidationError): + fs.params_pydantic_model.model_validate({"kwargs": {"first": "x"}}) + + def test_var_keyword_dict_annotation(): # Case 3: # A ``**kwargs: X`` annotation applies to each keyword *value* (PEP 484), so a