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.
Describe the bug
function_schema()detects the run-context parameter by looking only at the first parameter of the signature, without checking itsinspect.Parameter.kind. If that first parameter is annotatedRunContextWrapper/ToolContextbut is keyword-only,takes_contextis set toTrueand 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: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 defaultfailure_error_functionit 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", andto_call_argsskips it by index only, i.e. it assumes the value is passed positionally. The check infunction_schema()never verifies that assumption.Debug information
any-llm,litellm, orpydantic): not relevant — reproduced with no model/provider workagents.tool.invoke_function_tool, so no LLM call is involvedfbf59a40e9da5adb88d370fefaeaae0478376d4aNo traceback is produced. The
TypeErroris 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:Note the contrast: the
POSITIONAL_ONLYvariant (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 noctxproperty, confirming it was consumed as the context.Root cause:
src/agents/function_schema.py:397-403.first_param.kindis never consulted, so an annotation alone flipstakes_context = True; the block also never rejects a context parameter that cannot be passed positionally. The same file already rejectsRunContextWrapper/ToolContextat 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
Running this against
src/at the commit above prints the output shown under "Debug information". The same signature reached in the normal way (@function_toolon a keyword-only-API function, passed toAgent(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-2431That rule is exercised by
tests/test_function_tool_decorator.py(thekeyword-only-contextandvariadic-contextcases).docs/tools.md:380also states that functions "can optionally take the run context as their first argument", andFuncSchema.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 whenfirst_param.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD), otherwise raise the sameUserErrorwording at decoration time. (A more lenient alternative would keeptakes_context = Trueand pass the context under the parameter's own name at the call site intool.py, but raising at decoration time matchestool.py:2429and 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_argsplacingPOSITIONAL_ONLYargs intokeyword_argsand is a different code path.Happy to open a PR with the approach above if that would be useful.