Skip to content

fix: raise a clear error when on_invoke_tool gets a non-ToolContext#3681

Open
edwardwang-detecteng wants to merge 2 commits into
openai:mainfrom
edwardwang-detecteng:fix/clear-error-on-invoke-tool-non-context
Open

fix: raise a clear error when on_invoke_tool gets a non-ToolContext#3681
edwardwang-detecteng wants to merge 2 commits into
openai:mainfrom
edwardwang-detecteng:fix/clear-error-on-invoke-tool-non-context

Conversation

@edwardwang-detecteng

Copy link
Copy Markdown

Summary

Calling a function tool's on_invoke_tool(ctx, input_json) with a ctx that is not a
run context (most commonly None) currently fails far from the cause with a cryptic
error. This is easy to hit when unit-testing a @function_tool directly without going
through Runner.

Minimal reproduction (on main):

from agents import function_tool
import asyncio

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

asyncio.run(add.on_invoke_tool(None, '{"a": 1, "b": 2}'))

Before — a chained AttributeError that points nowhere near the real mistake:

File ".../agents/tool.py", line 1979, in _on_invoke_tool_impl
    tool_name = ctx.tool_name
AttributeError: 'NoneType' object has no attribute 'tool_name'

During handling of the above exception, another exception occurred:
...
File ".../agents/tool.py", line 1543, in _on_handled_error
    context.run_config is None or context.run_config.trace_include_sensitive_data
AttributeError: 'NoneType' object has no attribute 'run_config'

The first deref of ctx.tool_name raises, the wrapper's failure handler then dereferences
the same bad ctx again (context.run_config), and the second AttributeError is what
actually propagates — so the surfaced message is even further from the cause.

After — fail fast with one clear, actionable error:

TypeError: on_invoke_tool requires a ToolContext, got NoneType. Construct one with
ToolContext(context=..., tool_name=..., tool_call_id=..., tool_arguments=...) or invoke
the tool through Runner.

Root cause

_on_invoke_tool_impl reads ctx.tool_name with no validation. When ctx is not a real
context this raises AttributeError, and the wrapper's failure-handling path
(_on_handled_error) then dereferences ctx.run_config on the same bad value, masking the
original cause behind a second exception.

Fix

Validate the context once, at the top of the shared invocation boundary
(_FailureHandlingFunctionToolInvoker.__call__, i.e. the public on_invoke_tool), before
the try block, and raise a precise TypeError. The happy path is untouched (one
isinstance check) and the error propagates cleanly instead of being routed through the
failure formatter.

Notes / decisions for maintainers

  • Where to validate — wrapper vs. impl. I placed the guard in the wrapper rather than
    inside _on_invoke_tool_impl. The wrapper catches every exception from the impl and
    routes it through maybe_invoke_function_tool_failure_error_function; for a default
    function tool that converts the error into a model-facing string (and, with a bad ctx,
    crashes again in _on_handled_error). Raising before the try is the only place the
    clear error reliably propagates.
  • Accepted type — RunContextWrapper, not strictly ToolContext. The same wrapper is
    used by agent.as_tool(), whose invoker legitimately runs with a base
    RunContextWrapper (see _run_agent_impl, which guards with isinstance(context, ToolContext)). Requiring ToolContext here would break that path (and the existing
    test_agent_as_tool_preserves_scope_for_nested_run_context_wrapper). The check therefore
    accepts the base RunContextWrapper, which still rejects None/garbage, while the
    message points to ToolContext since that is what function-tool authors actually need.
  • Error type — TypeError. Chosen because passing the wrong type to a callable is the
    canonical TypeError case and is the most natural thing for developers unit-testing
    their tools to expect. The SDK's UserError was the alternative (there is precedent at
    tool_namespace()); happy to switch to UserError if maintainers prefer consistency.
  • Scope. This PR is the fail-fast fix only. A public test helper to synthesize a
    minimal ToolContext (so developers don't hand-build internal context) seems useful but
    is a feature, not a fix — I left it out and can open a separate issue/PR if there's
    interest.

Test plan

  • Added tests/test_function_tool.py::test_on_invoke_tool_rejects_non_tool_context,
    asserting the clear TypeError for None and another non-context type, and that a valid
    ToolContext still works.
  • make format — no changes; make lint — passed.
  • make typecheck — pyright clean; mypy reports only a pre-existing, unrelated error in
    tests/test_run_step_execution.py (asyncio.eager_task_factory under Python 3.11),
    present on main and untouched here.
  • make tests — full suite green (parallel + serial).

Issue number

No existing issue found for this specific failure (searched issues/PRs for
on_invoke_tool tool_name, ToolContext None, etc.).

Checks

  • I've added new tests (if relevant)
  • I've added/updated the relevant documentation (no user-facing behavior change for valid inputs)
  • I've run make lint and make format
  • I've made sure tests pass

@edwardwang-detecteng edwardwang-detecteng marked this pull request as ready for review June 23, 2026 23:15

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1d9a1029e1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/tool.py
# fast here with an actionable message before any tool logic runs. The base
# RunContextWrapper is accepted because agent-as-tool invokers run with one; the
# message points to ToolContext since that is what function tools need.
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 Reject base contexts for regular function tools

For a normal @function_tool, passing a plain RunContextWrapper is still the wrong direct-call context, but this guard lets it through because ToolContext subclasses RunContextWrapper. In that scenario _on_invoke_tool_impl still dereferences ctx.tool_name, and with the default failure handler _on_handled_error then dereferences context.run_config, so callers get the same masked AttributeError instead of the new clear TypeError; only agent-as-tool invokers should accept the base wrapper.

Useful? React with 👍 / 👎.

@seratch seratch changed the title Raise a clear error when on_invoke_tool gets a non-ToolContext fix: raise a clear error when on_invoke_tool gets a non-ToolContext Jun 23, 2026

@seratch seratch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The current guard still accepts a plain RunContextWrapper for an ordinary decorated function tool. That context lacks tool_name and run_config, so this case continues to produce the same masked AttributeError. Only agent-as-tool invokers should accept the base wrapper.

Before merging, please make the validation distinguish ordinary function tools from agent tools, add a regression test showing that a plain RunContextWrapper is rejected for a regular function tool, and keep the existing agent-as-tool compatibility path passing. After that focused change, this should be ready for another review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants