Skip to content

Python: [Bug]: Function call arguments that are not a JSON object reach the tool as {"raw": ...} instead of failing validation #9562

Description

Observed Behavior

When the model returns function call arguments that are not a JSON object, Content.parse_arguments reports no error. It wraps what it got in a dictionary with one key, raw: text that is not valid JSON becomes {"raw": "<the text>"}, and valid JSON that is not an object, such as a list or a string, becomes {"raw": <the value>}.

The automatic function-calling loop parses with that method and then runs its top-level checks on the result. For a tool declared with a JSON schema that has no required properties and does not set additionalProperties: false, the checks pass, because raw is an extra key and extra keys are allowed. The function is then invoked with raw=... as if the model had made a well-formed call. The function receives an argument it never declared, the model receives whatever the function made of it, and nothing tells the model that its arguments were malformed.

In our case the tools are proxied to MCP servers, so a malformed call reaches the server carrying a bogus raw argument. A missing required argument or a wrong type is answered with a validation error the model can correct on; malformed arguments are not.

Expected Behavior

Arguments that are not a JSON object are invalid arguments. The loop should answer them the way it answers a missing required property: an error result that names the cause, for example "arguments are not a JSON object", so the model can retry. If the raw wrapping stays for direct callers of parse_arguments, the loop should still not treat its result as valid arguments.

Steps to Reproduce

  1. Build a FunctionTool with the schema {"type": "object", "properties": {}}.
  2. Parse a function call whose arguments string is not {json, [1, 2] or "text".
  3. Invoke the tool with the parsed arguments, as the loop does.
  4. The function runs with {"raw": "not {json"}.

Minimal Reproduction

import asyncio
from typing import Any

from agent_framework import Content, FunctionTool

for text in ["not {json", "[1, 2]", '"text"']:
    call = Content.from_function_call("call-1", "list_accounts", arguments=text)
    print(f"{text!r:12} -> {call.parse_arguments()!r}")


async def list_accounts(**arguments: Any) -> str:
    print("tool called with", arguments)
    return "ok"


tool = FunctionTool(
    name="list_accounts",
    description="List the accounts",
    func=list_accounts,
    input_model={"type": "object", "properties": {}},
)
parsed = Content.from_function_call("call-1", "list_accounts", arguments="not {json").parse_arguments()
asyncio.run(tool.invoke(arguments=parsed))

Output on agent-framework-core 1.22.0:

'not {json'  -> {'raw': 'not {json'}
'[1, 2]'     -> {'raw': [1, 2]}
'"text"'     -> {'raw': 'text'}
tool called with {'raw': 'not {json'}

Where this happens on main at f7c6928:

  • parse_arguments returns the raw dictionary for a value that is not an object and for a decode error:
    if isinstance(self.arguments, str):
    # If arguments are a string, try to parse it as JSON
    try:
    loaded = json.loads(self.arguments)
    if isinstance(loaded, dict):
    return loaded # type: ignore[return-value]
    return {"raw": loaded}
    except (json.JSONDecodeError, TypeError):
    return {"raw": self.arguments}
  • The loop parses with it:
    parsed_args: dict[str, Any] = dict(function_call_content.parse_arguments() or {})
  • The top-level checks look at required, additionalProperties: false, enum and the property types only, so an undeclared raw key passes unless the schema forbids extra keys:
    """Run lightweight, top-level argument checks for schema-supplied tools."""
    parsed_arguments = dict(arguments)
    required_fields = [field for field in schema.get("required", []) if isinstance(field, str)]
    missing_fields = [field for field in required_fields if field not in parsed_arguments]
    if missing_fields:
    raise TypeError(f"Missing required argument(s) for '{tool_name}': {', '.join(sorted(missing_fields))}")
    properties: Mapping[str, Any] = schema.get("properties", {})
    if schema.get("additionalProperties") is False:
    unexpected_fields = sorted(field for field in parsed_arguments if field not in properties)
    if unexpected_fields:
    raise TypeError(f"Unexpected argument(s) for '{tool_name}': {', '.join(unexpected_fields)}")

Error Messages and Stack Traces

None. That is the problem: the malformed call is reported nowhere.

Package Versions

agent-framework-core: 1.22.0 (the code is the same on main at f7c6928)

Python Version

Python 3.12

Operating System

macOS

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 20, 2026
  2. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on Oct 20, 2026
  3. github-actions commented on Oct 20, 2026

    @github-actions
    Contributor

    🤖 Automated triage reproduction notes (agent-authored — trust but verify)

    Agent analysis

    Repro: python/packages/core/agent_framework/_tools.py::_auto_invoke_function at line 2242 accepts Content.parse_arguments() fallback mappings as ordinary arguments. Invalid JSON or valid non-object JSON triggers this when the tool has a permissive object schema, causing its callable to receive a synthetic raw keyword. Minimal repro: create that FunctionTool with a **kwargs callable and await _auto_invoke_function on arguments not {json; the callable receives {"raw": "not {json"}.

    • Failing test: python/packages/core/tests/core/test_tools.py
    • Files examined: python/AGENTS.md, python/packages/core/AGENTS.md, docs/specs/004-python-function-calling-loop.md, python/packages/core/agent_framework/_types.py, python/packages/core/agent_framework/_tools.py, python/packages/core/tests/core/test_tools.py, python/packages/core/pyproject.toml, python/pyproject.toml
    • Tests run: test_auto_invoke_preserves_explicit_null_argument, test_auto_invoke_rejects_arguments_that_are_not_json_objects
    • Reported version: 1.22.0
    • Current version: 1.22.0
  4. added
    agentsUsage: [Issues, PRs], Target: Single agent
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 20, 2026
  5. Madin-H23 commented on Oct 21, 2026

    @Madin-H23

    Fixed in PR #9573.

    The cause is where the report pointed: the loop used Content.parse_arguments() as the arguments of the call, and that method wraps a payload which is not a JSON object under a single "raw" key. For a tool whose schema has no required properties and does not set additionalProperties: false, the loop's own checks then passed — raw is just an extra key — so the tool was invoked with raw=... as if the model had made a well-formed call.

    The pull request parses the payload strictly in the loop and answers a call whose arguments are not a JSON object the way it already answers a missing required property: with an argument-validation error result that names the cause, so the model can retry.

    Two notes on the shape of the fix:

    • Content.parse_arguments() keeps its documented "raw" wrapping, so direct callers are unaffected; the check lives in the loop because that is the only place where the result becomes a tool invocation.
    • The error reuses the existing _FunctionArgumentValidationError path, so a malformed call produces the same kind of result as a missing required property, including the redacted message when argument snapshots are in play.

    The reproduction from the report is in the pull request as a test, for all three shapes (not {json, [1, 2] and "text"), plus a case that a well-formed call still reaches the tool with its arguments unchanged. On the unmodified file the tool is invoked with {'raw': 'not {json'} and with {'raw': [1, 2]}; with the change the tool is not invoked at all and the error names the cause.

    Thanks for the report — the raw fallback and the loop's use of it were the whole bug.

  6. danielquintas8 commented on Oct 21, 2026

    @danielquintas8
    Author

    H23 (@Madin-H23) Thanks for working on this. Has a maintainer confirmed the proposed direction? The contribution guide requires agreement before implementing non-trivial changes, or an explanation in the PR if the change qualifies as trivial. I could not find either here. Since this changes argument validation and whether tools execute, could we get maintainer confirmation?

  7. Madin-H23 commented on Oct 22, 2026

    @Madin-H23

    Thanks for the pointer — you are right, and we are not treating it as settled.

    To answer directly: no maintainer has confirmed the direction yet. The pull request reuses this issue (the first step the contribution guide asks for), and the triage bot has labelled the issue reproduced, but that is bot output, not maintainer agreement. The change also alters how the automatic function-calling loop validates arguments and whether a tool executes, so we do not think it qualifies as a trivial correction, and we are not claiming that it does.

    What we are doing now:

    • We are asking the maintainers for the ready-for-implementation label on this issue, so that the agreed direction is explicit before the pull request goes through review. SergeyMenshykh — could you add the label, or point us at who should decide it?
    • If no maintainer confirms, we will close the pull request rather than leave it open without the agreement the contribution guide asks for. We would rather not hold a slot in this repository's queue for a change the maintainers have not agreed to.
    • The patch is still being iterated on: an automated review of our current diff flagged an edge case — a whitespace-only argument payload still slips through the guard and reaches the tool as a raw argument — which we are fixing together with a regression test. So this is a request for the direction to be agreed, not a request to merge now.

    One piece of context so you can see the whole picture: the same applies to our other two open pull requests in this repository — #9571 for #9549 and #9574 for #9438. They are in the same state: an existing issue, the triage bot's reproduced label, and no maintainer agreement yet. (For #9549 the reporter also wrote in the issue that they would wait for explicit maintainer agreement before starting implementation, so asking is the only step we should have taken first.)

    If you read either change as trivial in the guide's sense — an obvious correction of a wrong result rather than a behaviour decision — say so and we will put that reasoning in the pull request description instead of asking for the label. Either way we will not keep a pull request open here without one of those two on record.

  8. Madin-H23 commented on Oct 22, 2026

    @Madin-H23

    Eduard van Valkenburg (@eavanvalkenburg) — one request about the label.

    Per the contribution guide, a non-trivial change needs a maintainer to mark the direction as ready for implementation before the pull request is opened. We did not wait for that here, and we are asking to correct it: if the direction in this issue is welcome for implementation, could you add the ready-for-implementation label? We asked SergeyMenshykh on 2026-10-22 as well, and the label history in this repository shows it comes from you.

    The pull request changes only how a function call whose arguments are not a JSON object is answered. The tool is no longer called with an undeclared raw argument; the model gets an error that names the reason and can retry from it. The public Content.parse_arguments() keeps its current wrapping, so direct callers are unaffected.

    #9573

    Either answer is fine. If there is no confirmation by 2026-10-27 we will close the pull request ourselves, as we said on this issue. We will not ask again.

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

Metadata

Metadata

Labels

agentsUsage: [Issues, PRs], Target: Single agentpythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions