33import asyncio
44import inspect
55import json
6+ import math
67import weakref
78from collections .abc import Awaitable , Mapping
89from dataclasses import dataclass , field
3536from . import _debug
3637from .computer import AsyncComputer , Computer
3738from .editor import ApplyPatchEditor , ApplyPatchOperation
38- from .exceptions import ModelBehaviorError , UserError
39+ from .exceptions import ModelBehaviorError , ToolTimeoutError , UserError
3940from .function_schema import DocstringStyle , function_schema
4041from .logger import logger
4142from .run_context import RunContextWrapper
6465]
6566
6667DEFAULT_APPROVAL_REJECTION_MESSAGE = "Tool execution was not approved."
68+ ToolTimeoutBehavior = Literal ["error_as_result" , "raise_exception" ]
69+ ToolErrorFunction = Callable [[RunContextWrapper [Any ], Exception ], MaybeAwaitable [str ]]
70+ _SYNC_FUNCTION_TOOL_MARKER = "__agents_sync_function_tool__"
6771
6872
6973class ToolOutputText (BaseModel ):
@@ -259,6 +263,20 @@ class FunctionTool:
259263 function that takes (run_context, tool_parameters, call_id) and returns whether this
260264 specific call needs approval."""
261265
266+ # Keep timeout fields after needs_approval to preserve positional constructor compatibility.
267+ timeout_seconds : float | None = None
268+ """Optional timeout (seconds) for each tool invocation."""
269+
270+ timeout_behavior : ToolTimeoutBehavior = "error_as_result"
271+ """How to handle timeout events.
272+
273+ - "error_as_result": return a model-visible timeout error string.
274+ - "raise_exception": raise a ToolTimeoutError and fail the run.
275+ """
276+
277+ timeout_error_function : ToolErrorFunction | None = None
278+ """Optional formatter for timeout errors when timeout_behavior is "error_as_result"."""
279+
262280 _is_agent_tool : bool = field (default = False , init = False , repr = False )
263281 """Internal flag indicating if this tool is an agent-as-tool."""
264282
@@ -271,6 +289,7 @@ class FunctionTool:
271289 def __post_init__ (self ):
272290 if self .strict_json_schema :
273291 self .params_json_schema = ensure_strict_json_schema (self .params_json_schema )
292+ _validate_function_tool_timeout_config (self )
274293
275294
276295@dataclass
@@ -905,8 +924,59 @@ def default_tool_error_function(ctx: RunContextWrapper[Any], error: Exception) -
905924 return f"An error occurred while running the tool. Please try again. Error: { str (error )} "
906925
907926
908- ToolErrorFunction = Callable [[RunContextWrapper [Any ], Exception ], MaybeAwaitable [str ]]
909927_UNSET_FAILURE_ERROR_FUNCTION = object ()
928+ _FUNCTION_TOOL_TIMEOUT_BEHAVIORS : tuple [ToolTimeoutBehavior , ...] = (
929+ "error_as_result" ,
930+ "raise_exception" ,
931+ )
932+
933+
934+ def default_tool_timeout_error_message (* , tool_name : str , timeout_seconds : float ) -> str :
935+ """Build the default message returned to the model when a tool times out."""
936+ return f"Tool '{ tool_name } ' timed out after { timeout_seconds :g} seconds."
937+
938+
939+ async def invoke_function_tool (
940+ * ,
941+ function_tool : FunctionTool ,
942+ context : ToolContext [Any ],
943+ arguments : str ,
944+ ) -> Any :
945+ """Invoke a function tool, enforcing timeout configuration when provided."""
946+ timeout_seconds = function_tool .timeout_seconds
947+ if timeout_seconds is None :
948+ return await function_tool .on_invoke_tool (context , arguments )
949+
950+ tool_task : asyncio .Future [Any ] = asyncio .ensure_future (
951+ function_tool .on_invoke_tool (context , arguments )
952+ )
953+ try :
954+ return await asyncio .wait_for (tool_task , timeout = timeout_seconds )
955+ except asyncio .TimeoutError as exc :
956+ if tool_task .done () and not tool_task .cancelled ():
957+ tool_exception = tool_task .exception ()
958+ if tool_exception is None :
959+ return tool_task .result ()
960+ raise tool_exception from None
961+
962+ timeout_error = ToolTimeoutError (
963+ tool_name = function_tool .name ,
964+ timeout_seconds = timeout_seconds ,
965+ )
966+ if function_tool .timeout_behavior == "raise_exception" :
967+ raise timeout_error from exc
968+
969+ timeout_error_function = function_tool .timeout_error_function
970+ if timeout_error_function is None :
971+ return default_tool_timeout_error_message (
972+ tool_name = function_tool .name ,
973+ timeout_seconds = timeout_seconds ,
974+ )
975+
976+ timeout_result = timeout_error_function (context , timeout_error )
977+ if inspect .isawaitable (timeout_result ):
978+ return await timeout_result
979+ return timeout_result
910980
911981
912982@overload
@@ -924,6 +994,9 @@ def function_tool(
924994 | Callable [[RunContextWrapper [Any ], dict [str , Any ], str ], Awaitable [bool ]] = False ,
925995 tool_input_guardrails : list [ToolInputGuardrail [Any ]] | None = None ,
926996 tool_output_guardrails : list [ToolOutputGuardrail [Any ]] | None = None ,
997+ timeout : float | None = None ,
998+ timeout_behavior : ToolTimeoutBehavior = "error_as_result" ,
999+ timeout_error_function : ToolErrorFunction | None = None ,
9271000) -> FunctionTool :
9281001 """Overload for usage as @function_tool (no parentheses)."""
9291002 ...
@@ -943,6 +1016,9 @@ def function_tool(
9431016 | Callable [[RunContextWrapper [Any ], dict [str , Any ], str ], Awaitable [bool ]] = False ,
9441017 tool_input_guardrails : list [ToolInputGuardrail [Any ]] | None = None ,
9451018 tool_output_guardrails : list [ToolOutputGuardrail [Any ]] | None = None ,
1019+ timeout : float | None = None ,
1020+ timeout_behavior : ToolTimeoutBehavior = "error_as_result" ,
1021+ timeout_error_function : ToolErrorFunction | None = None ,
9461022) -> Callable [[ToolFunction [...]], FunctionTool ]:
9471023 """Overload for usage as @function_tool(...)."""
9481024 ...
@@ -962,6 +1038,9 @@ def function_tool(
9621038 | Callable [[RunContextWrapper [Any ], dict [str , Any ], str ], Awaitable [bool ]] = False ,
9631039 tool_input_guardrails : list [ToolInputGuardrail [Any ]] | None = None ,
9641040 tool_output_guardrails : list [ToolOutputGuardrail [Any ]] | None = None ,
1041+ timeout : float | None = None ,
1042+ timeout_behavior : ToolTimeoutBehavior = "error_as_result" ,
1043+ timeout_error_function : ToolErrorFunction | None = None ,
9651044) -> FunctionTool | Callable [[ToolFunction [...]], FunctionTool ]:
9661045 """
9671046 Decorator to create a FunctionTool from a function. By default, we will:
@@ -1000,9 +1079,15 @@ def function_tool(
10001079 whether this specific call needs approval.
10011080 tool_input_guardrails: Optional list of guardrails to run before invoking the tool.
10021081 tool_output_guardrails: Optional list of guardrails to run after the tool returns.
1082+ timeout: Optional timeout in seconds for each tool call.
1083+ timeout_behavior: Timeout handling mode. "error_as_result" returns a model-visible message,
1084+ while "raise_exception" raises ToolTimeoutError and fails the run.
1085+ timeout_error_function: Optional formatter used for timeout messages when
1086+ timeout_behavior="error_as_result".
10031087 """
10041088
10051089 def _create_function_tool (the_func : ToolFunction [...]) -> FunctionTool :
1090+ is_sync_function_tool = not inspect .iscoroutinefunction (the_func )
10061091 schema = function_schema (
10071092 func = the_func ,
10081093 name_override = name_override ,
@@ -1043,7 +1128,7 @@ async def _on_invoke_tool_impl(ctx: ToolContext[Any], input: str) -> Any:
10431128 if not _debug .DONT_LOG_TOOL_DATA :
10441129 logger .debug (f"Tool call args: { args } , kwargs: { kwargs_dict } " )
10451130
1046- if inspect . iscoroutinefunction ( the_func ) :
1131+ if not is_sync_function_tool :
10471132 if schema .takes_context :
10481133 result = await the_func (ctx , * args , ** kwargs_dict )
10491134 else :
@@ -1106,6 +1191,9 @@ async def _on_invoke_tool(ctx: ToolContext[Any], input: str) -> Any:
11061191 )
11071192 return result
11081193
1194+ if is_sync_function_tool :
1195+ setattr (_on_invoke_tool , _SYNC_FUNCTION_TOOL_MARKER , True )
1196+
11091197 return FunctionTool (
11101198 name = schema .name ,
11111199 description = schema .description or "" ,
@@ -1116,6 +1204,9 @@ async def _on_invoke_tool(ctx: ToolContext[Any], input: str) -> Any:
11161204 needs_approval = needs_approval ,
11171205 tool_input_guardrails = tool_input_guardrails ,
11181206 tool_output_guardrails = tool_output_guardrails ,
1207+ timeout_seconds = timeout ,
1208+ timeout_behavior = timeout_behavior ,
1209+ timeout_error_function = timeout_error_function ,
11191210 )
11201211
11211212 # If func is actually a callable, we were used as @function_tool with no parentheses
@@ -1140,6 +1231,34 @@ def _is_computer_provider(candidate: object) -> bool:
11401231 )
11411232
11421233
1234+ def _validate_function_tool_timeout_config (tool : FunctionTool ) -> None :
1235+ timeout_seconds = tool .timeout_seconds
1236+ if timeout_seconds is not None :
1237+ if isinstance (timeout_seconds , bool ) or not isinstance (timeout_seconds , (int , float )):
1238+ raise TypeError (
1239+ "FunctionTool timeout_seconds must be a positive number in seconds or None."
1240+ )
1241+ timeout_seconds = float (timeout_seconds )
1242+ if not math .isfinite (timeout_seconds ):
1243+ raise ValueError ("FunctionTool timeout_seconds must be a finite number." )
1244+ if timeout_seconds <= 0 :
1245+ raise ValueError ("FunctionTool timeout_seconds must be greater than 0." )
1246+ if getattr (tool .on_invoke_tool , _SYNC_FUNCTION_TOOL_MARKER , False ):
1247+ raise ValueError (
1248+ "FunctionTool timeout_seconds is only supported for async @function_tool handlers."
1249+ )
1250+ tool .timeout_seconds = timeout_seconds
1251+
1252+ if tool .timeout_behavior not in _FUNCTION_TOOL_TIMEOUT_BEHAVIORS :
1253+ raise ValueError (
1254+ "FunctionTool timeout_behavior must be one of: "
1255+ + ", " .join (_FUNCTION_TOOL_TIMEOUT_BEHAVIORS )
1256+ )
1257+
1258+ if tool .timeout_error_function is not None and not callable (tool .timeout_error_function ):
1259+ raise TypeError ("FunctionTool timeout_error_function must be callable or None." )
1260+
1261+
11431262def _store_computer_initializer (tool : ComputerTool [Any ]) -> None :
11441263 config = tool .computer
11451264 if callable (config ) or _is_computer_provider (config ):
0 commit comments