fix: raise a clear error when on_invoke_tool gets a non-ToolContext#3681
Conversation
There was a problem hiding this comment.
💡 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".
| # 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): |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
Summary
Calling a function tool's
on_invoke_tool(ctx, input_json)with actxthat is not arun context (most commonly
None) currently fails far from the cause with a crypticerror. This is easy to hit when unit-testing a
@function_tooldirectly without goingthrough
Runner.Minimal reproduction (on
main):Before — a chained
AttributeErrorthat points nowhere near the real mistake:The first deref of
ctx.tool_nameraises, the wrapper's failure handler then dereferencesthe same bad
ctxagain (context.run_config), and the secondAttributeErroris whatactually propagates — so the surfaced message is even further from the cause.
After — fail fast with one clear, actionable error:
Root cause
_on_invoke_tool_implreadsctx.tool_namewith no validation. Whenctxis not a realcontext this raises
AttributeError, and the wrapper's failure-handling path(
_on_handled_error) then dereferencesctx.run_configon the same bad value, masking theoriginal cause behind a second exception.
Fix
Validate the context once, at the top of the shared invocation boundary
(
_FailureHandlingFunctionToolInvoker.__call__, i.e. the publicon_invoke_tool), beforethe
tryblock, and raise a preciseTypeError. The happy path is untouched (oneisinstancecheck) and the error propagates cleanly instead of being routed through thefailure formatter.
Notes / decisions for maintainers
inside
_on_invoke_tool_impl. The wrapper catches every exception from the impl androutes it through
maybe_invoke_function_tool_failure_error_function; for a defaultfunction tool that converts the error into a model-facing string (and, with a bad
ctx,crashes again in
_on_handled_error). Raising before thetryis the only place theclear error reliably propagates.
RunContextWrapper, not strictlyToolContext. The same wrapper isused by
agent.as_tool(), whose invoker legitimately runs with a baseRunContextWrapper(see_run_agent_impl, which guards withisinstance(context, ToolContext)). RequiringToolContexthere would break that path (and the existingtest_agent_as_tool_preserves_scope_for_nested_run_context_wrapper). The check thereforeaccepts the base
RunContextWrapper, which still rejectsNone/garbage, while themessage points to
ToolContextsince that is what function-tool authors actually need.TypeError. Chosen because passing the wrong type to a callable is thecanonical
TypeErrorcase and is the most natural thing for developers unit-testingtheir tools to expect. The SDK's
UserErrorwas the alternative (there is precedent attool_namespace()); happy to switch toUserErrorif maintainers prefer consistency.minimal
ToolContext(so developers don't hand-build internal context) seems useful butis a feature, not a fix — I left it out and can open a separate issue/PR if there's
interest.
Test plan
tests/test_function_tool.py::test_on_invoke_tool_rejects_non_tool_context,asserting the clear
TypeErrorforNoneand another non-context type, and that a validToolContextstill works.make format— no changes;make lint— passed.make typecheck— pyright clean; mypy reports only a pre-existing, unrelated error intests/test_run_step_execution.py(asyncio.eager_task_factoryunder Python 3.11),present on
mainand 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
make lintandmake format