From e92b299a951fcdb9f9228a8cd6fb67519560ffb5 Mon Sep 17 00:00:00 2001 From: subhash polisetti Date: Thu, 27 Aug 2026 23:34:12 -0700 Subject: [PATCH] fix(core): reject fixed-length tuple annotations for variadic tool arguments --- src/agents/function_schema.py | 8 ++++++ tests/test_function_schema.py | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/agents/function_schema.py b/src/agents/function_schema.py index 98e3200f07..8860c15180 100644 --- a/src/agents/function_schema.py +++ b/src/agents/function_schema.py @@ -441,6 +441,14 @@ def function_schema( args_of_tuple = get_args(ann) if len(args_of_tuple) == 2 and args_of_tuple[1] is Ellipsis: ann = list[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__"): + raise UserError( + f"Variadic parameter `*{name}` in function {func.__name__} is annotated" + f" with the fixed-length tuple `{ann}`. A variadic annotation describes" + " each positional argument, so use tuple[T, ...] or list[T] instead." + ) else: ann = list[Any] else: diff --git a/tests/test_function_schema.py b/tests/test_function_schema.py index 661fbcd06e..1d8325d1a0 100644 --- a/tests/test_function_schema.py +++ b/tests/test_function_schema.py @@ -462,6 +462,53 @@ def func(*args: tuple[int, ...]) -> int: fs.params_pydantic_model.model_validate({"args": [1, 2, 3]}) +def _var_positional_fixed_pair(*args: tuple[int, str]) -> int: + return len(args) + + +def _var_positional_fixed_single(*args: tuple[int]) -> int: + return len(args) + + +def _var_positional_empty_tuple(*args: tuple[()]) -> int: + return len(args) + + +@pytest.mark.parametrize( + "func", + [_var_positional_fixed_pair, _var_positional_fixed_single, _var_positional_empty_tuple], +) +def test_var_positional_fixed_length_tuple_annotation_is_rejected(func: Any): + # A fixed-length tuple cannot describe every positional argument, so reject it at + # construction instead of silently widening each argument to Any. tuple[()] reports no + # args yet is still parameterized, so it belongs in this group. + with pytest.raises(UserError, match=r"use tuple\[T, \.\.\.\] or list\[T\] instead"): + function_schema(func, use_docstring_info=False) + + +def _var_positional_homogeneous_tuple(*args: tuple[int, ...]) -> int: + return len(args) + + +def _var_positional_list(*args: list[int]) -> int: + return len(args) + + +def _var_positional_scalar(*args: int) -> int: + return len(args) + + +@pytest.mark.parametrize( + "func", + [_var_positional_homogeneous_tuple, _var_positional_list, _var_positional_scalar], +) +def test_var_positional_supported_annotations_still_build(func: Any): + # The alternatives named by the rejection message must keep working. + fs = function_schema(func, use_docstring_info=False) + + assert fs.params_json_schema["properties"]["args"]["type"] == "array" + + def test_var_keyword_dict_annotation(): # Case 3: # A ``**kwargs: X`` annotation applies to each keyword *value* (PEP 484), so a