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
6 changes: 6 additions & 0 deletions src/agents/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,12 @@ def __agents_bind_function_tool__(
return bound_invoker

async def __call__(self, ctx: ToolContext[Any], input: str) -> Any:
if not isinstance(ctx, RunContextWrapper):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require ToolContext for non-agent function tools

For a regular @function_tool, passing a plain RunContextWrapper is still a non-ToolContext direct invocation, but this guard treats it as valid; _on_invoke_tool_impl then immediately reads ctx.tool_name and the handled-error path can read ctx.run_config, so callers still get the same confusing/masked AttributeError instead of the clear TypeError. If base wrappers are only needed for agent.as_tool(), gate that exception on the bound FunctionTool; otherwise require ToolContext here.

Useful? React with 👍 / 👎.

raise TypeError(
f"on_invoke_tool requires a ToolContext, got {type(ctx).__name__}. "
"Construct one with ToolContext(context=..., tool_name=..., "
"tool_call_id=..., tool_arguments=...) or invoke the tool through Runner."
)
try:
return await self._invoke_tool_impl(ctx, input)
except Exception as e:
Expand Down
33 changes: 33 additions & 0 deletions tests/test_function_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
)
from agents.tool import default_tool_error_function
from agents.tool_context import ToolContext
from openai.types.responses import ResponseFunctionToolCall


def argless_function() -> str:
Expand Down Expand Up @@ -1063,3 +1064,35 @@ def test_function_tool_timeout_error_function_must_be_callable() -> None:
on_invoke_tool=_noop_on_invoke_tool,
timeout_error_function=cast(Any, "not-callable"),
)


async def test_on_invoke_tool_rejects_non_tool_context() -> None:
"""Calling on_invoke_tool with a non-context value should fail fast and clearly."""

@function_tool
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b

with pytest.raises(TypeError, match="on_invoke_tool requires a ToolContext"):
await add.on_invoke_tool(cast(Any, None), '{"a": 1, "b": 2}')

with pytest.raises(TypeError, match="on_invoke_tool requires a ToolContext"):
await add.on_invoke_tool(cast(Any, "not a context"), '{"a": 1, "b": 2}')

# A valid ToolContext should still work.
tool_call = ResponseFunctionToolCall(
type="function_call",
name="add",
call_id="call-add",
arguments='{"a": 1, "b": 2}',
)
tool_context = ToolContext(
context=None,
tool_name="add",
tool_call_id="call-add",
tool_arguments=tool_call.arguments,
tool_call=tool_call,
)
result = await add.on_invoke_tool(tool_context, tool_call.arguments)
assert result == 3