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 @@ -455,6 +455,14 @@ class FunctionTool:
)
"""Optional callback that attaches SDK-only custom data to the tool output item."""

function: ToolFunction[...] | None = field(default=None, kw_only=True, repr=False)
"""The original Python callable wrapped by `function_tool`, when available.

This is `None` for tools that are not backed by a plain function (for example,
agent-as-tool or hosted-tool wrappers). It gives code outside the Agents
runtime a stable hook to introspect, directly test, or re-run the underlying
function without going through a `ToolContext`/JSON round-trip."""

_failure_error_function: ToolErrorFunction | None = field(
default=None,
kw_only=True,
Expand Down Expand Up @@ -619,6 +627,7 @@ def _build_wrapped_function_tool(
sync_invoker: bool = False,
mcp_title: str | None = None,
tool_origin: ToolOrigin | None = None,
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 @@ -644,6 +653,7 @@ def _build_wrapped_function_tool(
timeout_error_function=timeout_error_function,
defer_loading=defer_loading,
custom_data_extractor=custom_data_extractor,
function=function,
_mcp_title=mcp_title,
_tool_origin=tool_origin,
),
Expand Down Expand Up @@ -2033,6 +2043,7 @@ async def _on_invoke_tool_impl(ctx: ToolContext[Any], input: str) -> Any:
timeout_error_function=timeout_error_function,
defer_loading=defer_loading,
custom_data_extractor=custom_data_extractor,
function=the_func,
sync_invoker=is_sync_function_tool,
)
return function_tool
Expand Down
45 changes: 45 additions & 0 deletions tests/test_function_tool_exposes_function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Tests for FunctionTool.function — public access to the wrapped callable (#3381)."""

from __future__ import annotations

from collections.abc import Awaitable, Callable
from typing import cast

from agents import FunctionTool, function_tool


def test_sync_function_tool_exposes_underlying_function() -> None:
@function_tool
def add(x: int, y: int) -> int:
"""Add two numbers."""
return x + y

assert add.function is not None
# The original callable is reachable and runnable without a ToolContext.
fn = cast(Callable[[int, int], int], add.function)
assert fn(2, 3) == 5


async def test_async_function_tool_exposes_underlying_function() -> None:
@function_tool
async def slow_add(x: int, y: int) -> int:
"""Add two numbers."""
return x + y

assert slow_add.function is not None
fn = cast(Callable[[int, int], Awaitable[int]], slow_add.function)
assert await fn(2, 3) == 5


def test_function_tool_function_defaults_to_none() -> None:
# Tools not built from a plain function (constructed directly) expose None.
async def _invoke(ctx: object, input: str) -> str:
return "ok"

tool = FunctionTool(
name="manual",
description="",
params_json_schema={"type": "object", "properties": {}, "additionalProperties": False},
on_invoke_tool=_invoke,
)
assert tool.function is None