Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/agents/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,14 @@ class FunctionTool:
_emit_tool_origin: bool = field(default=True, kw_only=True, repr=False)
"""Whether runtime item generation should emit tool origin metadata for this tool."""

_func: ToolFunction[...] | None = field(default=None, kw_only=True, repr=False)
"""Original callable when this tool was created from @function_tool."""

@property
def func(self) -> ToolFunction[...] | None:
"""Return the original callable for tools created with @function_tool."""
return self._func

@property
def qualified_name(self) -> str:
"""Return the public qualified name used to identify this function tool."""
Expand Down Expand Up @@ -514,6 +522,7 @@ def _build_wrapped_function_tool(
sync_invoker: bool = False,
mcp_title: str | None = None,
tool_origin: ToolOrigin | None = None,
original_function: ToolFunction[...] | None = None,
) -> FunctionTool:
"""Create a FunctionTool with copied-tool-aware failure handling bound in one place."""
on_invoke_tool = _with_context_function_tool_failure_error_handler(
Expand All @@ -540,6 +549,7 @@ def _build_wrapped_function_tool(
defer_loading=defer_loading,
_mcp_title=mcp_title,
_tool_origin=tool_origin,
_func=original_function,
),
failure_error_function,
)
Expand Down Expand Up @@ -1905,6 +1915,7 @@ async def _on_invoke_tool_impl(ctx: ToolContext[Any], input: str) -> Any:
timeout_error_function=timeout_error_function,
defer_loading=defer_loading,
sync_invoker=is_sync_function_tool,
original_function=the_func,
)
return function_tool

Expand Down
20 changes: 20 additions & 0 deletions tests/test_function_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,26 @@ async def test_simple_function():
)


def test_function_tool_exposes_original_function_without_wrapping() -> None:
tool = function_tool(simple_function)

assert tool.func is simple_function
assert not callable(tool)
assert copy.copy(tool).func is simple_function
assert dataclasses.replace(tool, name="renamed").func is simple_function


def test_manual_function_tool_has_no_original_function() -> None:
tool = FunctionTool(
name="manual",
description="manual tool",
params_json_schema={"type": "object", "properties": {}, "additionalProperties": False},
on_invoke_tool=lambda _ctx, _input: asyncio.sleep(0),
)

assert tool.func is None


@pytest.mark.asyncio
async def test_sync_function_runs_via_to_thread(monkeypatch: pytest.MonkeyPatch) -> None:
calls = {"to_thread": 0, "func": 0}
Expand Down