From 51cdb5b324897d5b582248f65d67abfaba5ad519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Georg=20W=C3=B6lflein?= Date: Fri, 6 Feb 2026 11:50:19 +0100 Subject: [PATCH 1/3] feat: support Annotated[T, Field(...)] in function_schema for tool parameters - Preserve per-parameter Annotated metadata and extract FieldInfo when building fields - Add _extract_field_info_from_metadata() helper; use it in normal-parameter branch - Merge annotated Field with docstring description and optional signature default - Add tests for required/optional constraints, string constraints, and multiple params --- src/agents/function_schema.py | 26 ++++- tests/test_function_schema.py | 179 ++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 1 deletion(-) diff --git a/src/agents/function_schema.py b/src/agents/function_schema.py index b9331da87d..cff7f987e6 100644 --- a/src/agents/function_schema.py +++ b/src/agents/function_schema.py @@ -210,6 +210,15 @@ def _extract_description_from_metadata(metadata: tuple[Any, ...]) -> str | None: return None +def _extract_field_info_from_metadata(metadata: tuple[Any, ...]) -> FieldInfo | None: + """Returns the first FieldInfo in Annotated metadata, or None.""" + + for item in metadata: + if isinstance(item, FieldInfo): + return item + return None + + def function_schema( func: Callable[..., Any], docstring_style: DocstringStyle | None = None, @@ -252,6 +261,7 @@ def function_schema( type_hints_with_extras = get_type_hints(func, include_extras=True) type_hints: dict[str, Any] = {} annotated_param_descs: dict[str, str] = {} + param_metadata: dict[str, tuple[Any, ...]] = {} for name, annotation in type_hints_with_extras.items(): if name == "return": @@ -259,6 +269,7 @@ def function_schema( stripped_ann, metadata = _strip_annotated(annotation) type_hints[name] = stripped_ann + param_metadata[name] = metadata description = _extract_description_from_metadata(metadata) if description is not None: @@ -356,7 +367,20 @@ def function_schema( else: # Normal parameter - if default == inspect._empty: + metadata = param_metadata.get(name, ()) + field_info_from_annotated = _extract_field_info_from_metadata(metadata) + + if field_info_from_annotated is not None: + merged = FieldInfo.merge_field_infos( + field_info_from_annotated, + description=field_description or field_info_from_annotated.description, + ) + if default != inspect._empty and not isinstance(default, FieldInfo): + merged = FieldInfo.merge_field_infos(merged, default=default) + elif isinstance(default, FieldInfo): + merged = FieldInfo.merge_field_infos(merged, default) + fields[name] = (ann, merged) + elif default == inspect._empty: # Required field fields[name] = ( ann, diff --git a/tests/test_function_schema.py b/tests/test_function_schema.py index 40607b9bd6..9771bda99d 100644 --- a/tests/test_function_schema.py +++ b/tests/test_function_schema.py @@ -706,3 +706,182 @@ def func_with_multiple_field_constraints( with pytest.raises(ValidationError): # zero factor fs.params_pydantic_model(**{"score": 50, "factor": 0.0}) + + +# --- Annotated + Field: same behavior as Field as default --- + + +def test_function_with_annotated_field_required_constraints(): + """Test function with required Annotated[int, Field(...)] parameter that has constraints.""" + + def func_with_annotated_field_constraints( + my_number: Annotated[int, Field(..., gt=10, le=100)], + ) -> int: + return my_number * 2 + + fs = function_schema(func_with_annotated_field_constraints, use_docstring_info=False) + + # Check that the schema includes the constraints + properties = fs.params_json_schema.get("properties", {}) + my_number_schema = properties.get("my_number", {}) + assert my_number_schema.get("type") == "integer" + assert my_number_schema.get("exclusiveMinimum") == 10 # gt=10 + assert my_number_schema.get("maximum") == 100 # le=100 + + # Valid input should work + valid_input = {"my_number": 50} + parsed = fs.params_pydantic_model(**valid_input) + args, kwargs_dict = fs.to_call_args(parsed) + result = func_with_annotated_field_constraints(*args, **kwargs_dict) + assert result == 100 + + # Invalid input: too small (should violate gt=10) + with pytest.raises(ValidationError): + fs.params_pydantic_model(**{"my_number": 5}) + + # Invalid input: too large (should violate le=100) + with pytest.raises(ValidationError): + fs.params_pydantic_model(**{"my_number": 150}) + + +def test_function_with_annotated_field_optional_with_default(): + """Optional Annotated[float, Field(...)] param with default and constraints.""" + + def func_with_annotated_optional_field( + required_param: str, + optional_param: Annotated[float, Field(default=5.0, ge=0.0)], + ) -> str: + return f"{required_param}: {optional_param}" + + fs = function_schema(func_with_annotated_optional_field, use_docstring_info=False) + + # Check that the schema includes the constraints and description + properties = fs.params_json_schema.get("properties", {}) + optional_schema = properties.get("optional_param", {}) + assert optional_schema.get("type") == "number" + assert optional_schema.get("minimum") == 0.0 # ge=0.0 + assert optional_schema.get("default") == 5.0 + + # Valid input with default + valid_input = {"required_param": "test"} + parsed = fs.params_pydantic_model(**valid_input) + args, kwargs_dict = fs.to_call_args(parsed) + result = func_with_annotated_optional_field(*args, **kwargs_dict) + assert result == "test: 5.0" + + # Valid input with explicit value + valid_input2 = {"required_param": "test", "optional_param": 10.5} + parsed2 = fs.params_pydantic_model(**valid_input2) + args2, kwargs_dict2 = fs.to_call_args(parsed2) + result2 = func_with_annotated_optional_field(*args2, **kwargs_dict2) + assert result2 == "test: 10.5" + + # Invalid input: negative value (should violate ge=0.0) + with pytest.raises(ValidationError): + fs.params_pydantic_model(**{"required_param": "test", "optional_param": -1.0}) + + +def test_function_with_annotated_field_string_constraints(): + """Annotated[str, Field(...)] parameter with string constraints (min/max length, pattern).""" + + def func_with_annotated_string_field( + name: Annotated[ + str, + Field(..., min_length=3, max_length=20, pattern=r"^[A-Za-z]+$"), + ], + ) -> str: + return f"Hello, {name}!" + + fs = function_schema(func_with_annotated_string_field, use_docstring_info=False) + + # Check that the schema includes string constraints + properties = fs.params_json_schema.get("properties", {}) + name_schema = properties.get("name", {}) + assert name_schema.get("type") == "string" + assert name_schema.get("minLength") == 3 + assert name_schema.get("maxLength") == 20 + assert name_schema.get("pattern") == r"^[A-Za-z]+$" + + # Valid input + valid_input = {"name": "Alice"} + parsed = fs.params_pydantic_model(**valid_input) + args, kwargs_dict = fs.to_call_args(parsed) + result = func_with_annotated_string_field(*args, **kwargs_dict) + assert result == "Hello, Alice!" + + # Invalid input: too short + with pytest.raises(ValidationError): + fs.params_pydantic_model(**{"name": "Al"}) + + # Invalid input: too long + with pytest.raises(ValidationError): + fs.params_pydantic_model(**{"name": "A" * 25}) + + # Invalid input: doesn't match pattern (contains numbers) + with pytest.raises(ValidationError): + fs.params_pydantic_model(**{"name": "Alice123"}) + + +def test_function_with_annotated_field_multiple_constraints(): + """Test function with multiple Annotated params with Field having different constraint types.""" + + def func_with_annotated_multiple_field_constraints( + score: Annotated[ + int, + Field(..., ge=0, le=100, description="Score from 0 to 100"), + ], + name: Annotated[str, Field(default="Unknown", min_length=1, max_length=50)], + factor: Annotated[float, Field(default=1.0, gt=0.0, description="Positive multiplier")], + ) -> str: + final_score = score * factor + return f"{name} scored {final_score}" + + fs = function_schema(func_with_annotated_multiple_field_constraints, use_docstring_info=False) + + # Check schema structure + properties = fs.params_json_schema.get("properties", {}) + + # Check score field + score_schema = properties.get("score", {}) + assert score_schema.get("type") == "integer" + assert score_schema.get("minimum") == 0 + assert score_schema.get("maximum") == 100 + assert score_schema.get("description") == "Score from 0 to 100" + + # Check name field + name_schema = properties.get("name", {}) + assert name_schema.get("type") == "string" + assert name_schema.get("minLength") == 1 + assert name_schema.get("maxLength") == 50 + assert name_schema.get("default") == "Unknown" + + # Check factor field + factor_schema = properties.get("factor", {}) + assert factor_schema.get("type") == "number" + assert factor_schema.get("exclusiveMinimum") == 0.0 + assert factor_schema.get("default") == 1.0 + assert factor_schema.get("description") == "Positive multiplier" + + # Valid input with defaults + valid_input = {"score": 85} + parsed = fs.params_pydantic_model(**valid_input) + args, kwargs_dict = fs.to_call_args(parsed) + result = func_with_annotated_multiple_field_constraints(*args, **kwargs_dict) + assert result == "Unknown scored 85.0" + + # Valid input with all parameters + valid_input2 = {"score": 90, "name": "Alice", "factor": 1.5} + parsed2 = fs.params_pydantic_model(**valid_input2) + args2, kwargs_dict2 = fs.to_call_args(parsed2) + result2 = func_with_annotated_multiple_field_constraints(*args2, **kwargs_dict2) + assert result2 == "Alice scored 135.0" + + # Test various validation errors + with pytest.raises(ValidationError): # score too high + fs.params_pydantic_model(**{"score": 150}) + + with pytest.raises(ValidationError): # empty name + fs.params_pydantic_model(**{"score": 50, "name": ""}) + + with pytest.raises(ValidationError): # zero factor + fs.params_pydantic_model(**{"score": 50, "factor": 0.0}) From 117290a8b3ab6ef253a9e1fdc7708e8b1b3a1ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Georg=20W=C3=B6lflein?= Date: Fri, 6 Feb 2026 12:07:05 +0100 Subject: [PATCH 2/3] docs: add Pydantic Field subsection for tool arguments in tools.md Document both default-based and Annotated forms for constraining and describing function-tool arguments (addresses review request in PR #1124). --- docs/tools.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/tools.md b/docs/tools.md index 284aed912a..839722c451 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -278,6 +278,26 @@ As mentioned before, we automatically parse the function signature to extract th The code for the schema extraction lives in [`agents.function_schema`][]. +### Constraining and describing arguments with Pydantic Field + +You can use Pydantic's [`Field`](https://docs.pydantic.dev/latest/concepts/fields/) to add constraints (e.g. min/max for numbers, length or pattern for strings) and descriptions to tool arguments. As in Pydantic, both forms are supported: default-based (`arg: int = Field(..., ge=1)`) and `Annotated` (`arg: Annotated[int, Field(..., ge=1)]`). The generated JSON schema and validation include these constraints. + +```python +from typing import Annotated +from pydantic import Field +from agents import function_tool + +# Default-based form +@function_tool +def score_a(score: int = Field(..., ge=0, le=100, description="Score from 0 to 100")) -> str: + return f"Score recorded: {score}" + +# Annotated form +@function_tool +def score_b(score: Annotated[int, Field(..., ge=0, le=100, description="Score from 0 to 100")]) -> str: + return f"Score recorded: {score}" +``` + ## Agents as tools In some workflows, you may want a central agent to orchestrate a network of specialized agents, instead of handing off control. You can do this by modeling agents as tools. From 6620a9a2c0a5143f84a2998a592c3e2a58cdef8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Georg=20W=C3=B6lflein?= Date: Fri, 6 Feb 2026 12:13:24 +0100 Subject: [PATCH 3/3] Update wording in docs --- docs/tools.md | 104 +++++++++++++++++++++++++------------------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/docs/tools.md b/docs/tools.md index 839722c451..9f135534ac 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -2,21 +2,21 @@ Tools let agents take actions: things like fetching data, running code, calling external APIs, and even using a computer. The SDK supports five categories: -- Hosted OpenAI tools: run alongside the model on OpenAI servers. -- Local runtime tools: run in your environment (computer use, shell, apply patch). -- Function calling: wrap any Python function as a tool. -- Agents as tools: expose an agent as a callable tool without a full handoff. -- Experimental: Codex tool: run workspace-scoped Codex tasks from a tool call. +- Hosted OpenAI tools: run alongside the model on OpenAI servers. +- Local runtime tools: run in your environment (computer use, shell, apply patch). +- Function calling: wrap any Python function as a tool. +- Agents as tools: expose an agent as a callable tool without a full handoff. +- Experimental: Codex tool: run workspace-scoped Codex tasks from a tool call. ## Hosted tools OpenAI offers a few built-in tools when using the [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel]: -- The [`WebSearchTool`][agents.tool.WebSearchTool] lets an agent search the web. -- The [`FileSearchTool`][agents.tool.FileSearchTool] allows retrieving information from your OpenAI Vector Stores. -- The [`CodeInterpreterTool`][agents.tool.CodeInterpreterTool] lets the LLM execute code in a sandboxed environment. -- The [`HostedMCPTool`][agents.tool.HostedMCPTool] exposes a remote MCP server's tools to the model. -- The [`ImageGenerationTool`][agents.tool.ImageGenerationTool] generates images from a prompt. +- The [`WebSearchTool`][agents.tool.WebSearchTool] lets an agent search the web. +- The [`FileSearchTool`][agents.tool.FileSearchTool] allows retrieving information from your OpenAI Vector Stores. +- The [`CodeInterpreterTool`][agents.tool.CodeInterpreterTool] lets the LLM execute code in a sandboxed environment. +- The [`HostedMCPTool`][agents.tool.HostedMCPTool] exposes a remote MCP server's tools to the model. +- The [`ImageGenerationTool`][agents.tool.ImageGenerationTool] generates images from a prompt. ```python from agents import Agent, FileSearchTool, Runner, WebSearchTool @@ -41,9 +41,9 @@ async def main(): Local runtime tools execute in your environment and require you to supply implementations: -- [`ComputerTool`][agents.tool.ComputerTool]: implement the [`Computer`][agents.computer.Computer] or [`AsyncComputer`][agents.computer.AsyncComputer] interface to enable GUI/browser automation. -- [`ShellTool`][agents.tool.ShellTool] or [`LocalShellTool`][agents.tool.LocalShellTool]: provide a shell executor to run commands. -- [`ApplyPatchTool`][agents.tool.ApplyPatchTool]: implement [`ApplyPatchEditor`][agents.editor.ApplyPatchEditor] to apply diffs locally. +- [`ComputerTool`][agents.tool.ComputerTool]: implement the [`Computer`][agents.computer.Computer] or [`AsyncComputer`][agents.computer.AsyncComputer] interface to enable GUI/browser automation. +- [`ShellTool`][agents.tool.ShellTool] or [`LocalShellTool`][agents.tool.LocalShellTool]: provide a shell executor to run commands. +- [`ApplyPatchTool`][agents.tool.ApplyPatchTool]: implement [`ApplyPatchEditor`][agents.editor.ApplyPatchEditor] to apply diffs locally. ```python from agents import Agent, ApplyPatchTool, ShellTool @@ -89,10 +89,10 @@ agent = Agent( You can use any Python function as a tool. The Agents SDK will setup the tool automatically: -- The name of the tool will be the name of the Python function (or you can provide a name) -- Tool description will be taken from the docstring of the function (or you can provide a description) -- The schema for the function inputs is automatically created from the function's arguments -- Descriptions for each input are taken from the docstring of the function, unless disabled +- The name of the tool will be the name of the Python function (or you can provide a name) +- Tool description will be taken from the docstring of the function (or you can provide a description) +- The schema for the function inputs is automatically created from the function's arguments +- Descriptions for each input are taken from the docstring of the function, unless disabled We use Python's `inspect` module to extract the function signature, along with [`griffe`](https://mkdocstrings.github.io/griffe/) to parse docstrings and `pydantic` for schema creation. @@ -225,18 +225,18 @@ for tool in agent.tools: In addition to returning text outputs, you can return one or many images or files as the output of a function tool. To do so, you can return any of: -- Images: [`ToolOutputImage`][agents.tool.ToolOutputImage] (or the TypedDict version, [`ToolOutputImageDict`][agents.tool.ToolOutputImageDict]) -- Files: [`ToolOutputFileContent`][agents.tool.ToolOutputFileContent] (or the TypedDict version, [`ToolOutputFileContentDict`][agents.tool.ToolOutputFileContentDict]) -- Text: either a string or stringable objects, or [`ToolOutputText`][agents.tool.ToolOutputText] (or the TypedDict version, [`ToolOutputTextDict`][agents.tool.ToolOutputTextDict]) +- Images: [`ToolOutputImage`][agents.tool.ToolOutputImage] (or the TypedDict version, [`ToolOutputImageDict`][agents.tool.ToolOutputImageDict]) +- Files: [`ToolOutputFileContent`][agents.tool.ToolOutputFileContent] (or the TypedDict version, [`ToolOutputFileContentDict`][agents.tool.ToolOutputFileContentDict]) +- Text: either a string or stringable objects, or [`ToolOutputText`][agents.tool.ToolOutputText] (or the TypedDict version, [`ToolOutputTextDict`][agents.tool.ToolOutputTextDict]) ### Custom function tools Sometimes, you don't want to use a Python function as a tool. You can directly create a [`FunctionTool`][agents.tool.FunctionTool] if you prefer. You'll need to provide: -- `name` -- `description` -- `params_json_schema`, which is the JSON schema for the arguments -- `on_invoke_tool`, which is an async function that receives a [`ToolContext`][agents.tool_context.ToolContext] and the arguments as a JSON string, and must return the tool output as a string. +- `name` +- `description` +- `params_json_schema`, which is the JSON schema for the arguments +- `on_invoke_tool`, which is an async function that receives a [`ToolContext`][agents.tool_context.ToolContext] and the arguments as a JSON string, and must return the tool output as a string. ```python from typing import Any @@ -278,7 +278,7 @@ As mentioned before, we automatically parse the function signature to extract th The code for the schema extraction lives in [`agents.function_schema`][]. -### Constraining and describing arguments with Pydantic Field +### Constraining argument values You can use Pydantic's [`Field`](https://docs.pydantic.dev/latest/concepts/fields/) to add constraints (e.g. min/max for numbers, length or pattern for strings) and descriptions to tool arguments. As in Pydantic, both forms are supported: default-based (`arg: int = Field(..., ge=1)`) and `Annotated` (`arg: Annotated[int, Field(..., ge=1)]`). The generated JSON schema and validation include these constraints. @@ -398,9 +398,9 @@ See `examples/agent_patterns/agents_as_tools_structured.py` for a complete runna In certain cases, you might want to modify the output of the tool-agents before returning it to the central agent. This may be useful if you want to: -- Extract a specific piece of information (e.g., a JSON payload) from the sub-agent's chat history. -- Convert or reformat the agent’s final answer (e.g., transform Markdown into plain text or CSV). -- Validate the output or provide a fallback value when the agent’s response is missing or malformed. +- Extract a specific piece of information (e.g., a JSON payload) from the sub-agent's chat history. +- Convert or reformat the agent’s final answer (e.g., transform Markdown into plain text or CSV). +- Validate the output or provide a fallback value when the agent’s response is missing or malformed. You can do this by supplying the `custom_output_extractor` argument to the `as_tool` method: @@ -508,16 +508,16 @@ asyncio.run(main()) The `is_enabled` parameter accepts: -- **Boolean values**: `True` (always enabled) or `False` (always disabled) -- **Callable functions**: Functions that take `(context, agent)` and return a boolean -- **Async functions**: Async functions for complex conditional logic +- **Boolean values**: `True` (always enabled) or `False` (always disabled) +- **Callable functions**: Functions that take `(context, agent)` and return a boolean +- **Async functions**: Async functions for complex conditional logic Disabled tools are completely hidden from the LLM at runtime, making this useful for: -- Feature gating based on user permissions -- Environment-specific tool availability (dev vs prod) -- A/B testing different tool configurations -- Dynamic tool filtering based on runtime state +- Feature gating based on user permissions +- Environment-specific tool availability (dev vs prod) +- A/B testing different tool configurations +- Dynamic tool filtering based on runtime state ## Experimental: Codex tool @@ -553,28 +553,28 @@ agent = Agent( What to know: -- Auth: set `CODEX_API_KEY` (preferred) or `OPENAI_API_KEY`, or pass `codex_options={"api_key": "..."}`. -- Runtime: `codex_options.base_url` overrides the CLI base URL. -- Binary resolution: set `codex_options.codex_path_override` (or `CODEX_PATH`) to pin the CLI path. Otherwise the SDK resolves `codex` from `PATH`, then falls back to the bundled vendor binary. -- Environment: `codex_options.env` fully controls the subprocess environment. When it is provided, the subprocess does not inherit `os.environ`. -- Stream limits: `codex_options.codex_subprocess_stream_limit_bytes` (or `OPENAI_AGENTS_CODEX_SUBPROCESS_STREAM_LIMIT_BYTES`) controls stdout/stderr reader limits. Valid range is `65536` to `67108864`; default is `8388608`. -- Inputs: tool calls must include at least one item in `inputs` with `{ "type": "text", "text": ... }` or `{ "type": "local_image", "path": ... }`. -- Thread defaults: configure `default_thread_options` for `model_reasoning_effort`, `web_search_mode` (preferred over legacy `web_search_enabled`), `approval_policy`, and `additional_directories`. -- Turn defaults: configure `default_turn_options` for `idle_timeout_seconds` and cancellation `signal`. -- Safety: pair `sandbox_mode` with `working_directory`; set `skip_git_repo_check=True` outside Git repos. -- Behavior: `persist_session=True` reuses a single Codex thread and returns its `thread_id`. -- Streaming: `on_stream` receives Codex events (reasoning, command execution, MCP tool calls, file changes, web search). -- Outputs: results include `response`, `usage`, and `thread_id`; usage is added to `RunContextWrapper.usage`. -- Structure: `output_schema` enforces structured Codex responses when you need typed outputs. -- See `examples/tools/codex.py` for a complete runnable sample. +- Auth: set `CODEX_API_KEY` (preferred) or `OPENAI_API_KEY`, or pass `codex_options={"api_key": "..."}`. +- Runtime: `codex_options.base_url` overrides the CLI base URL. +- Binary resolution: set `codex_options.codex_path_override` (or `CODEX_PATH`) to pin the CLI path. Otherwise the SDK resolves `codex` from `PATH`, then falls back to the bundled vendor binary. +- Environment: `codex_options.env` fully controls the subprocess environment. When it is provided, the subprocess does not inherit `os.environ`. +- Stream limits: `codex_options.codex_subprocess_stream_limit_bytes` (or `OPENAI_AGENTS_CODEX_SUBPROCESS_STREAM_LIMIT_BYTES`) controls stdout/stderr reader limits. Valid range is `65536` to `67108864`; default is `8388608`. +- Inputs: tool calls must include at least one item in `inputs` with `{ "type": "text", "text": ... }` or `{ "type": "local_image", "path": ... }`. +- Thread defaults: configure `default_thread_options` for `model_reasoning_effort`, `web_search_mode` (preferred over legacy `web_search_enabled`), `approval_policy`, and `additional_directories`. +- Turn defaults: configure `default_turn_options` for `idle_timeout_seconds` and cancellation `signal`. +- Safety: pair `sandbox_mode` with `working_directory`; set `skip_git_repo_check=True` outside Git repos. +- Behavior: `persist_session=True` reuses a single Codex thread and returns its `thread_id`. +- Streaming: `on_stream` receives Codex events (reasoning, command execution, MCP tool calls, file changes, web search). +- Outputs: results include `response`, `usage`, and `thread_id`; usage is added to `RunContextWrapper.usage`. +- Structure: `output_schema` enforces structured Codex responses when you need typed outputs. +- See `examples/tools/codex.py` for a complete runnable sample. ## Handling errors in function tools When you create a function tool via `@function_tool`, you can pass a `failure_error_function`. This is a function that provides an error response to the LLM in case the tool call crashes. -- By default (i.e. if you don't pass anything), it runs a `default_tool_error_function` which tells the LLM an error occurred. -- If you pass your own error function, it runs that instead, and sends the response to the LLM. -- If you explicitly pass `None`, then any tool call errors will be re-raised for you to handle. This could be a `ModelBehaviorError` if the model produced invalid JSON, or a `UserError` if your code crashed, etc. +- By default (i.e. if you don't pass anything), it runs a `default_tool_error_function` which tells the LLM an error occurred. +- If you pass your own error function, it runs that instead, and sends the response to the LLM. +- If you explicitly pass `None`, then any tool call errors will be re-raised for you to handle. This could be a `ModelBehaviorError` if the model produced invalid JSON, or a `UserError` if your code crashed, etc. ```python from agents import function_tool, RunContextWrapper