From 2d7f2a1a704dbed77e2ee091185679191e59f8da Mon Sep 17 00:00:00 2001 From: Varun Ursekar <159173215+varunursekar@users.noreply.github.com> Date: Wed, 3 Dec 2025 23:52:28 +0000 Subject: [PATCH 1/4] store reference --- src/agents/tool.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/agents/tool.py b/src/agents/tool.py index 499a84045..9bc1f8cff 100644 --- a/src/agents/tool.py +++ b/src/agents/tool.py @@ -179,6 +179,10 @@ class FunctionTool: and returns whether the tool is enabled. You can use this to dynamically enable/disable a tool based on your context/state.""" + func: ToolFunction[...] | None = None + """The function that implements the tool. Ensures that a reference to the original function exists + when @function_tool is used.""" + # Tool-specific guardrails tool_input_guardrails: list[ToolInputGuardrail[Any]] | None = None """Optional list of input guardrails to run before invoking this tool.""" @@ -661,6 +665,7 @@ async def _on_invoke_tool(ctx: ToolContext[Any], input: str) -> Any: on_invoke_tool=_on_invoke_tool, strict_json_schema=strict_mode, is_enabled=is_enabled, + func=func ) # If func is actually a callable, we were used as @function_tool with no parentheses From 583d001485207f9cfe7ad9820c8fd7701f505e0b Mon Sep 17 00:00:00 2001 From: Varun Ursekar <159173215+varunursekar@users.noreply.github.com> Date: Thu, 4 Dec 2025 00:51:07 +0000 Subject: [PATCH 2/4] fix linting --- src/agents/tool.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/agents/tool.py b/src/agents/tool.py index 9bc1f8cff..cda52f674 100644 --- a/src/agents/tool.py +++ b/src/agents/tool.py @@ -180,8 +180,8 @@ class FunctionTool: based on your context/state.""" func: ToolFunction[...] | None = None - """The function that implements the tool. Ensures that a reference to the original function exists - when @function_tool is used.""" + """The function that implements the tool. Ensures that a reference to the + original function exists when @function_tool is used.""" # Tool-specific guardrails tool_input_guardrails: list[ToolInputGuardrail[Any]] | None = None @@ -665,7 +665,7 @@ async def _on_invoke_tool(ctx: ToolContext[Any], input: str) -> Any: on_invoke_tool=_on_invoke_tool, strict_json_schema=strict_mode, is_enabled=is_enabled, - func=func + func=func, ) # If func is actually a callable, we were used as @function_tool with no parentheses From b48ed6f771c1383ec6c38df1d15c01502c59b556 Mon Sep 17 00:00:00 2001 From: Varun Ursekar <159173215+varunursekar@users.noreply.github.com> Date: Thu, 4 Dec 2025 06:29:24 +0000 Subject: [PATCH 3/4] comments --- src/agents/tool.py | 16 ++++++++++-- tests/test_function_tool.py | 50 +++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/agents/tool.py b/src/agents/tool.py index cda52f674..54e932c04 100644 --- a/src/agents/tool.py +++ b/src/agents/tool.py @@ -1,5 +1,6 @@ from __future__ import annotations +import functools import inspect import json from collections.abc import Awaitable @@ -179,7 +180,7 @@ class FunctionTool: and returns whether the tool is enabled. You can use this to dynamically enable/disable a tool based on your context/state.""" - func: ToolFunction[...] | None = None + _func: ToolFunction[...] | None = field(default=None, repr=False) """The function that implements the tool. Ensures that a reference to the original function exists when @function_tool is used.""" @@ -194,6 +195,17 @@ def __post_init__(self): if self.strict_json_schema: self.params_json_schema = ensure_strict_json_schema(self.params_json_schema) + if self._func: + functools.update_wrapper(self, self._func) + + def __call__(self, *args, **kwargs): + if not self._func: + raise AttributeError("""FunctionTool has no attribute `_func` and is not callable. + Likely because it was created directly without the + @function_tool decorator.""") + + return self._func(*args, **kwargs) + @dataclass class FileSearchTool: @@ -665,7 +677,7 @@ async def _on_invoke_tool(ctx: ToolContext[Any], input: str) -> Any: on_invoke_tool=_on_invoke_tool, strict_json_schema=strict_mode, is_enabled=is_enabled, - func=func, + _func=func, ) # If func is actually a callable, we were used as @function_tool with no parentheses diff --git a/tests/test_function_tool.py b/tests/test_function_tool.py index 18107773d..a72ea2be6 100644 --- a/tests/test_function_tool.py +++ b/tests/test_function_tool.py @@ -1,4 +1,6 @@ +import inspect import json +from dataclasses import asdict from typing import Any import pytest @@ -81,6 +83,44 @@ async def test_simple_function(): ToolContext(None, tool_name=tool.name, tool_call_id="1", tool_arguments=""), "" ) + # Direct call + result = tool(2, 2) + assert result == 4 + + +async def async_function(a: int, b: int = 5): + return a + b + + +@pytest.mark.asyncio +async def test_async_function(): + tool = function_tool(async_function, failure_error_function=None) + assert tool.name == "async_function" + + result = await tool.on_invoke_tool( + ToolContext(None, tool_name=tool.name, tool_call_id="1", tool_arguments='{"a": 1}'), + '{"a": 1}', + ) + assert result == 6 + + result = await tool.on_invoke_tool( + ToolContext(None, tool_name=tool.name, tool_call_id="1", tool_arguments='{"a": 1, "b": 2}'), + '{"a": 1, "b": 2}', + ) + assert result == 3 + + # Missing required argument should raise an error + with pytest.raises(ModelBehaviorError): + await tool.on_invoke_tool( + ToolContext(None, tool_name=tool.name, tool_call_id="1", tool_arguments=""), "" + ) + + # Direct call + result = await tool(2, 2) + assert result == 4 + + assert not inspect.iscoroutinefunction(tool.__call__), "tool.__call__ should sync." + class Foo(BaseModel): a: int @@ -148,6 +188,16 @@ async def test_complex_args_function(): ) +def test_absent_func_tool(): + tool = function_tool(simple_function) + kwargs = asdict(tool) + kwargs.pop("_func") + manually_defined_tool = FunctionTool(**kwargs) + + with pytest.raises(AttributeError, match="not callable"): + manually_defined_tool(1, 1) + + def test_function_config_overrides(): tool = function_tool(simple_function, name_override="custom_name") assert tool.name == "custom_name" From 78234654fa623e5bab29df3eb19fdede48f49883 Mon Sep 17 00:00:00 2001 From: Varun Ursekar <159173215+varunursekar@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:25:03 +0000 Subject: [PATCH 4/4] fix test --- tests/extensions/memory/test_advanced_sqlite_session.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/extensions/memory/test_advanced_sqlite_session.py b/tests/extensions/memory/test_advanced_sqlite_session.py index 40edb99fe..bd09fbff9 100644 --- a/tests/extensions/memory/test_advanced_sqlite_session.py +++ b/tests/extensions/memory/test_advanced_sqlite_session.py @@ -20,7 +20,7 @@ @function_tool -async def test_tool(query: str) -> str: +async def _test_tool(query: str) -> str: """A test tool for testing tool call tracking.""" return f"Tool result for: {query}" @@ -28,7 +28,7 @@ async def test_tool(query: str) -> str: @pytest.fixture def agent() -> Agent: """Fixture for a basic agent with a fake model.""" - return Agent(name="test", model=FakeModel(), tools=[test_tool]) + return Agent(name="test", model=FakeModel(), tools=[_test_tool]) @pytest.fixture @@ -961,7 +961,7 @@ async def test_tool_execution_integration(agent: Agent): [ { # type: ignore "type": "function_call", - "name": "test_tool", + "name": "_test_tool", "arguments": '{"query": "test query"}', "call_id": "call_123", }