Skip to content

function_schema treats a keyword-only RunContextWrapper first parameter as context, so every call to that tool raises a TypeError that is swallowed into the tool result #5053

Description

@BlueX888

Describe the bug

function_schema() detects the run-context parameter by looking only at the first parameter of the signature, without checking its inspect.Parameter.kind. If that first parameter is annotated RunContextWrapper / ToolContext but is keyword-only, takes_context is set to True and the parameter is dropped from the generated JSON schema. The generated call then passes the context positionally (src/agents/tool.py:2732, result = await the_func(ctx, *args, **kwargs_dict)), which is illegal for a keyword-only parameter:

@function_tool
def search(*, ctx: RunContextWrapper[Any], query: str) -> str:
    ...

Decorating this function raises nothing at definition time. Every invocation raises TypeError: search() takes 0 positional arguments but 1 positional argument (and 1 keyword-only argument) were given, and because that exception goes through the default failure_error_function it is converted into a tool-result string rather than propagating. The caller sees a normal tool result, the model sees an error on every single call, and the tool never works.

FuncSchema.takes_context's docstring says the argument "must be the first argument", and to_call_args skips it by index only, i.e. it assumes the value is passed positionally. The check in function_schema() never verifies that assumption.

Debug information

  • Agents SDK version: 0.22.2
  • Related library versions (optional, e.g. any-llm, litellm, or pydantic): not relevant — reproduced with no model/provider work
  • Python version: 3.13.15
  • Operating system: macOS (Darwin 25.6.0)
  • Model and model provider: none — the tool is invoked directly through agents.tool.invoke_function_tool, so no LLM call is involved
  • Does the issue reproduce with the latest Agents SDK release? Yes, on 0.22.2 at commit fbf59a40e9da5adb88d370fefaeaae0478376d4a
  • Does the issue occur consistently or intermittently? Consistently, on every call

No traceback is produced. The TypeError is caught by the default failure handler and returned to the model as a tool result, so the failure is silent at the call site. Verbatim output observed:

kwonly_ctx OK An error occurred while running the tool. Please try again. Error: kwonly_ctx() takes 0 positional arguments but 1 positional argument (and 1 keyword-only argument) were given
positional_only_ctx OK got 2
schema kwonly: {"properties": {"a": {"title": "A", "type": "integer"}}, "required": ["a"], "title": "kwonly_ctx_args", "type": "object", "additionalProperties": false}

Note the contrast: the POSITIONAL_ONLY variant (def positional_only_ctx(ctx: RunContextWrapper, /, a: int)) works, so the defect is specific to the keyword-only kind. The generated schema for the keyword-only variant also contains no ctx property, confirming it was consumed as the context.

Root cause: src/agents/function_schema.py:397-403. first_param.kind is never consulted, so an annotation alone flips takes_context = True; the block also never rejects a context parameter that cannot be passed positionally. The same file already rejects RunContextWrapper/ToolContext at every non-first position (function_schema.py:409-416), so a keyword-only context parameter is accepted only because it happens to sit at index 0.

Repro steps

import asyncio, json
from agents import function_tool
from agents.tool import invoke_function_tool
from agents.tool_context import ToolContext
from agents.run_context import RunContextWrapper

@function_tool
def kwonly_ctx(*, ctx: RunContextWrapper, a: int) -> str:
    """Tool."""
    return f"got {a}"

@function_tool
def positional_only_ctx(ctx: RunContextWrapper, /, a: int) -> str:
    """Tool."""
    return f"got {a}"

async def main():
    ctx = ToolContext(context=None, tool_name="t", tool_call_id="1", tool_arguments="{}")
    for tool, args in ((kwonly_ctx, {"a": 1}), (positional_only_ctx, {"a": 2})):
        try:
            out = await invoke_function_tool(function_tool=tool, context=ctx, arguments=json.dumps(args))
            print(tool.name, "OK", out)
        except Exception as e:
            print(tool.name, "EXC", type(e).__name__, e)
    print("schema kwonly:", json.dumps(kwonly_ctx.params_json_schema))
asyncio.run(main())

Running this against src/ at the commit above prints the output shown under "Debug information". The same signature reached in the normal way (@function_tool on a keyword-only-API function, passed to Agent(tools=[...])) fails identically on the first real tool call.

Expected behavior

Decorating such a function should either work or fail loudly at decoration time, not produce a tool that raises on every invocation and reports only an opaque error string to the model.

The concrete basis is the sibling path in the same release. The callable-object context check validates the parameter kind and rejects anything that is not positional:

src/agents/tool.py:2423-2431

if index == 0 and parameter.kind in (
    inspect.Parameter.POSITIONAL_ONLY,
    inspect.Parameter.POSITIONAL_OR_KEYWORD,
):
    continue
raise UserError(
    "Unsupported callable object context parameter: RunContextWrapper or ToolContext "
    "must be the first positional parameter. Use an explicit wrapper function."
)

That rule is exercised by tests/test_function_tool_decorator.py (the keyword-only-context and variadic-context cases). docs/tools.md:380 also states that functions "can optionally take the run context as their first argument", and FuncSchema.takes_context's docstring says the argument "must be the first argument" — i.e. the plain-function path is the one out of step with the documented and already-enforced rule.

The smallest fix mirrors the callable-object rule: in function_schema(), accept the annotation as the context only when first_param.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD), otherwise raise the same UserError wording at decoration time. (A more lenient alternative would keep takes_context = True and pass the context under the parameter's own name at the call site in tool.py, but raising at decoration time matches tool.py:2429 and the existing non-first-position check in this same file.)

I searched the issue tracker for existing reports before filing and found none for this signature; the closest is the closed #2922, which concerns to_call_args placing POSITIONAL_ONLY args into keyword_args and is a different code path.

Happy to open a PR with the approach above if that would be useful.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions